mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
fix(google-meet): surface browser create manual actions
This commit is contained in:
@@ -117,6 +117,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/TTS: suppress successful spoken transcripts from verbose chat tool output when structured voice media is already queued, while preserving text output for non-builtin tool-name collisions. Fixes #71282. Thanks @neeravmakwana.
|
||||
- Plugins/Google Meet: reuse existing Meet tabs and active sessions across harmless URL query differences, avoiding duplicate Chrome windows when agents retry a join. Thanks @steipete.
|
||||
- Plugins/Google Meet: tell agents to recover already-open Meet tabs after browser timeouts, and make the dev CLI release its build lock if compiler spawning fails. Thanks @steipete.
|
||||
- Plugins/Google Meet: return structured manual-action details when browser-based meeting creation needs login or permissions, so agents can guide the operator without opening duplicate Meet tabs. Thanks @steipete.
|
||||
- Plugins/CLI: provide Gateway-backed node inspection to plugin commands, so `googlemeet recover-tab` can inspect paired browser nodes from the terminal. Thanks @steipete.
|
||||
- Gateway/sessions: recover main-agent turns interrupted by a gateway restart from stale transcript-lock evidence, avoiding stuck `status: "running"` sessions without broad post-boot transcript scans. Fixes #70555. Thanks @bitloi.
|
||||
- Codex approvals: sanitize MCP elicitation approval titles, descriptions, and display parameters before forwarding them to OpenClaw approval prompts. (#71343) Thanks @Lucenx9.
|
||||
|
||||
@@ -512,6 +512,30 @@ Example JSON output from the browser fallback:
|
||||
}
|
||||
```
|
||||
|
||||
If the browser fallback hits Google login or a Meet permission blocker before it
|
||||
can create the URL, the Gateway method returns a failed response and the
|
||||
`google_meet` tool returns structured details instead of a plain string:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "browser",
|
||||
"error": "google-login-required: Sign in to Google in the OpenClaw browser profile, then retry meeting creation.",
|
||||
"manualActionRequired": true,
|
||||
"manualActionReason": "google-login-required",
|
||||
"manualActionMessage": "Sign in to Google in the OpenClaw browser profile, then retry meeting creation.",
|
||||
"browser": {
|
||||
"nodeId": "ba0f4e4bc...",
|
||||
"targetId": "tab-1",
|
||||
"browserUrl": "https://accounts.google.com/signin",
|
||||
"browserTitle": "Sign in - Google Accounts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When an agent sees `manualActionRequired: true`, it should report the
|
||||
`manualActionMessage` plus the browser node/tab context and stop opening new
|
||||
Meet tabs until the operator completes the browser step.
|
||||
|
||||
Example JSON output from API create:
|
||||
|
||||
```json
|
||||
@@ -888,6 +912,10 @@ to the pinned Chrome node browser. Confirm:
|
||||
- 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 the tool returns `manualActionRequired: true`, use
|
||||
the returned `browser.nodeId`, `browser.targetId`, `browserUrl`, and
|
||||
`manualActionMessage` to guide the operator. Do not retry in a loop until that
|
||||
action is complete.
|
||||
- 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
|
||||
|
||||
@@ -197,6 +197,80 @@ describe("google-meet create flow", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("reports structured manual action when browser creation needs Google login", async () => {
|
||||
const { methods } = setup(
|
||||
{
|
||||
defaultTransport: "chrome-node",
|
||||
chromeNode: { node: "parallels-macos" },
|
||||
},
|
||||
{
|
||||
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: {
|
||||
result: {
|
||||
targetId: "login-tab",
|
||||
title: "New Tab",
|
||||
url: proxy.body?.url,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (proxy.path === "/act") {
|
||||
return {
|
||||
payload: {
|
||||
result: {
|
||||
ok: true,
|
||||
targetId: "login-tab",
|
||||
result: {
|
||||
manualActionReason: "google-login-required",
|
||||
manualAction:
|
||||
"Sign in to Google in the OpenClaw browser profile, then retry meeting creation.",
|
||||
browserUrl: "https://accounts.google.com/signin",
|
||||
browserTitle: "Sign in - Google Accounts",
|
||||
notes: ["Sign-in page detected."],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
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(false);
|
||||
expect(respond.mock.calls[0]?.[1]).toMatchObject({
|
||||
source: "browser",
|
||||
error:
|
||||
"google-login-required: Sign in to Google in the OpenClaw browser profile, then retry meeting creation.",
|
||||
manualActionRequired: true,
|
||||
manualActionReason: "google-login-required",
|
||||
manualActionMessage:
|
||||
"Sign in to Google in the OpenClaw browser profile, then retry meeting creation.",
|
||||
browser: {
|
||||
nodeId: "node-1",
|
||||
targetId: "login-tab",
|
||||
browserUrl: "https://accounts.google.com/signin",
|
||||
browserTitle: "Sign in - Google Accounts",
|
||||
notes: ["Sign-in page detected."],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("creates and joins a Meet through the create tool action by default", async () => {
|
||||
const { tools, nodesInvoke } = setup(
|
||||
{
|
||||
@@ -292,6 +366,71 @@ describe("google-meet create flow", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("returns structured manual action from the create tool action", async () => {
|
||||
const { tools } = setup(
|
||||
{
|
||||
defaultTransport: "chrome-node",
|
||||
chromeNode: { node: "parallels-macos" },
|
||||
},
|
||||
{
|
||||
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: {
|
||||
result: {
|
||||
targetId: "permission-tab",
|
||||
title: "Meet",
|
||||
url: proxy.body?.url,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (proxy.path === "/act") {
|
||||
return {
|
||||
payload: {
|
||||
result: {
|
||||
ok: true,
|
||||
targetId: "permission-tab",
|
||||
result: {
|
||||
manualActionReason: "meet-permission-required",
|
||||
manualAction:
|
||||
"Allow microphone/camera permissions for Meet in the OpenClaw browser profile, then retry meeting creation.",
|
||||
browserUrl: "https://meet.google.com/new",
|
||||
browserTitle: "Meet",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected browser proxy path ${proxy.path}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
const tool = tools[0] as {
|
||||
execute: (id: string, params: unknown) => Promise<{ details: Record<string, unknown> }>;
|
||||
};
|
||||
|
||||
const result = await tool.execute("id", { action: "create" });
|
||||
|
||||
expect(result.details).toMatchObject({
|
||||
source: "browser",
|
||||
manualActionRequired: true,
|
||||
manualActionReason: "meet-permission-required",
|
||||
manualActionMessage:
|
||||
"Allow microphone/camera permissions for Meet in the OpenClaw browser profile, then retry meeting creation.",
|
||||
browser: {
|
||||
nodeId: "node-1",
|
||||
targetId: "permission-tab",
|
||||
browserUrl: "https://meet.google.com/new",
|
||||
browserTitle: "Meet",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reuses an existing browser create tab instead of opening duplicates", async () => {
|
||||
const { methods, nodesInvoke } = setup(
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ import { buildGoogleMeetPreflightReport, fetchGoogleMeetSpace } from "./src/meet
|
||||
import { handleGoogleMeetNodeHostCommand } from "./src/node-host.js";
|
||||
import { resolveGoogleMeetAccessToken } from "./src/oauth.js";
|
||||
import { GoogleMeetRuntime } from "./src/runtime.js";
|
||||
import { isGoogleMeetBrowserManualActionError } from "./src/transports/chrome-create.js";
|
||||
|
||||
const googleMeetConfigSchema = {
|
||||
parse(value: unknown) {
|
||||
@@ -250,8 +251,11 @@ export default definePluginEntry({
|
||||
return runtime;
|
||||
};
|
||||
|
||||
const formatGatewayError = (err: unknown) =>
|
||||
isGoogleMeetBrowserManualActionError(err) ? err.payload : { error: formatErrorMessage(err) };
|
||||
|
||||
const sendError = (respond: (ok: boolean, payload?: unknown) => void, err: unknown) => {
|
||||
respond(false, { error: formatErrorMessage(err) });
|
||||
respond(false, formatGatewayError(err));
|
||||
};
|
||||
|
||||
api.registerGatewayMethod(
|
||||
@@ -485,7 +489,7 @@ export default definePluginEntry({
|
||||
throw new Error("unknown google_meet action");
|
||||
}
|
||||
} catch (err) {
|
||||
return json({ error: formatErrorMessage(err) });
|
||||
return json(formatGatewayError(err));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -35,6 +35,42 @@ export type GoogleMeetBrowserCreateResult = {
|
||||
source: "browser";
|
||||
};
|
||||
|
||||
export type GoogleMeetBrowserManualAction = {
|
||||
source: "browser";
|
||||
error: string;
|
||||
manualActionRequired: true;
|
||||
manualActionReason?: GoogleMeetChromeHealth["manualActionReason"];
|
||||
manualActionMessage: string;
|
||||
browser: {
|
||||
nodeId: string;
|
||||
targetId?: string;
|
||||
browserUrl?: string;
|
||||
browserTitle?: string;
|
||||
notes?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export class GoogleMeetBrowserManualActionError extends Error {
|
||||
readonly payload: GoogleMeetBrowserManualAction;
|
||||
|
||||
constructor(payload: Omit<GoogleMeetBrowserManualAction, "source" | "error">) {
|
||||
const prefix = payload.manualActionReason ? `${payload.manualActionReason}: ` : "";
|
||||
super(`${prefix}${payload.manualActionMessage}`);
|
||||
this.name = "GoogleMeetBrowserManualActionError";
|
||||
this.payload = {
|
||||
source: "browser",
|
||||
error: this.message,
|
||||
...payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function isGoogleMeetBrowserManualActionError(
|
||||
error: unknown,
|
||||
): error is GoogleMeetBrowserManualActionError {
|
||||
return error instanceof GoogleMeetBrowserManualActionError;
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -298,10 +334,18 @@ export async function createMeetWithBrowserProxyOnNode(params: {
|
||||
};
|
||||
}
|
||||
if (result.manualAction) {
|
||||
if (result.manualActionReason) {
|
||||
throw new Error(`${result.manualActionReason}: ${result.manualAction}`);
|
||||
}
|
||||
throw new Error(result.manualAction);
|
||||
throw new GoogleMeetBrowserManualActionError({
|
||||
manualActionRequired: true,
|
||||
manualActionReason: result.manualActionReason,
|
||||
manualActionMessage: result.manualAction,
|
||||
browser: {
|
||||
nodeId,
|
||||
targetId,
|
||||
browserUrl: result.browserUrl,
|
||||
browserTitle: result.browserTitle,
|
||||
notes: [...notes],
|
||||
},
|
||||
});
|
||||
}
|
||||
await sleep(result.retryAfterMs ?? GOOGLE_MEET_BROWSER_POLL_MS);
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user