fix(google-meet): join created meetings by default

This commit is contained in:
Peter Steinberger
2026-04-25 01:31:26 +01:00
parent d12987d725
commit 7c0549bd9f
5 changed files with 263 additions and 34 deletions

View File

@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/failover: forward embedded run abort signals into provider-owned model streams, cap implicit LLM idle watchdogs below long run timeouts, and mark 429 responses without usable retry timing as non-retryable so GitHub Copilot rate limits fail over or surface promptly instead of hanging until run timeout. Fixes #71120.
- Plugins/Google Meet: make meeting creation join by default, with an explicit URL-only opt-out, so agents that create a Meet also enter it.
- Browser/tool: keep explicit AI snapshots from inheriting the efficient role-snapshot default and preserve numeric Playwright AI refs, so `--format ai` remains a real AI snapshot path. Fixes #62550. Thanks @ly85206559.
- Gateway/config: keep in-process config patch reload comparisons on the resolved source snapshot when `${VAR}` env refs are restored on disk, avoiding false full gateway restarts for unchanged gateway/plugin secrets. Fixes #71208. Thanks @robbiethompson18.
- Slack/messages: serialize write-client requests and whole outbound sends per target so rapid multi-message Slack replies preserve send order. Fixes #69101. (#69105) Thanks @nightq and @ztexydt-cqh.

View File

@@ -97,11 +97,16 @@ Or let an agent join through the `google_meet` tool:
}
```
Create a new meeting, then join it:
Create a new meeting and join it:
```bash
openclaw googlemeet create
openclaw googlemeet join https://meet.google.com/new-abcd-xyz --transport chrome-node
openclaw googlemeet create --transport chrome-node --mode realtime
```
Create only the URL without joining:
```bash
openclaw googlemeet create --no-join
```
`googlemeet create` has two paths:
@@ -115,24 +120,18 @@ openclaw googlemeet join https://meet.google.com/new-abcd-xyz --transport chrome
Browser automation handles Meet's own first-run microphone prompt; that prompt
is not treated as a Google login failure.
The command output includes a `source` field (`api` or `browser`) so agents can
explain which path was used.
The command/tool output includes a `source` field (`api` or `browser`) so agents
can explain which path was used. `create` joins the new meeting by default and
returns `joined: true` plus the join session. To only mint the URL, use
`create --no-join` on the CLI or pass `"join": false` to the tool.
Or tell an agent: "Create a Google Meet, join it with realtime voice, and send
me the link." The agent should call `google_meet` with `action: "create"`, copy
the returned `meetingUri`, then call `google_meet` with `action: "join"` and
that URL.
me the link." The agent should call `google_meet` with `action: "create"` and
then share the returned `meetingUri`.
```json
{
"action": "create"
}
```
```json
{
"action": "join",
"url": "https://meet.google.com/new-abcd-xyz",
"action": "create",
"transport": "chrome-node",
"mode": "realtime"
}
@@ -475,11 +474,11 @@ Create a fresh Meet space:
openclaw googlemeet create
```
The command prints the new `meeting uri` and source. With OAuth credentials it
uses the official Google Meet API. Without OAuth credentials it uses the pinned
Chrome node's signed-in browser profile as a fallback. Agents can use the
`google_meet` tool with `action: "create"` to create a meeting, then call
`action: "join"` with the returned `meetingUri`.
The command prints the new `meeting uri`, source, and join session. With OAuth
credentials it uses the official Google Meet API. Without OAuth credentials it
uses the pinned Chrome node's signed-in browser profile as a fallback. Agents can
use the `google_meet` tool with `action: "create"` to create and join in one
step. For URL-only creation, pass `"join": false`.
Example JSON output from the browser fallback:
@@ -487,9 +486,16 @@ Example JSON output from the browser fallback:
{
"source": "browser",
"meetingUri": "https://meet.google.com/abc-defg-hij",
"joined": true,
"browser": {
"nodeId": "ba0f4e4bc...",
"targetId": "tab-1"
},
"join": {
"session": {
"id": "meet_...",
"url": "https://meet.google.com/abc-defg-hij"
}
}
}
```
@@ -500,19 +506,26 @@ Example JSON output from API create:
{
"source": "api",
"meetingUri": "https://meet.google.com/abc-defg-hij",
"joined": true,
"space": {
"name": "spaces/abc-defg-hij",
"meetingCode": "abc-defg-hij",
"meetingUri": "https://meet.google.com/abc-defg-hij"
},
"join": {
"session": {
"id": "meet_...",
"url": "https://meet.google.com/abc-defg-hij"
}
}
}
```
Creating a Meet only creates or discovers the meeting URL. The Chrome or
Chrome-node transport still needs a signed-in Google Chrome profile to join
through the browser. If the profile is signed out, OpenClaw reports
`manualActionRequired: true` or a browser fallback error and asks the operator
to finish Google login before retrying.
Creating a Meet joins by default. The Chrome or Chrome-node transport still
needs a signed-in Google Chrome profile to join through the browser. If the
profile is signed out, OpenClaw reports `manualActionRequired: true` or a
browser fallback error and asks the operator to finish Google login before
retrying.
Set `preview.enrollmentAcknowledged: true` only after confirming your Cloud
project, OAuth principal, and meeting participants are enrolled in the Google

View File

@@ -803,7 +803,7 @@ describe("google-meet plugin", () => {
});
try {
await program.parseAsync(["googlemeet", "create"], { from: "user" });
await program.parseAsync(["googlemeet", "create", "--no-join"], { from: "user" });
expect(stdout.output()).toContain("meeting uri: https://meet.google.com/new-abcd-xyz");
expect(stdout.output()).toContain("space: spaces/new-space");
} finally {
@@ -811,7 +811,7 @@ describe("google-meet plugin", () => {
}
});
it("creates a Meet through browser fallback when OAuth is not configured", async () => {
it("can create a Meet through browser fallback without joining when requested", async () => {
const { methods, nodesInvoke } = setup(
{
defaultTransport: "chrome-node",
@@ -861,12 +861,13 @@ describe("google-meet plugin", () => {
| undefined;
const respond = vi.fn();
await handler?.({ params: {}, respond });
await handler?.({ params: { join: false }, respond });
expect(respond.mock.calls[0]?.[0]).toBe(true);
expect(respond.mock.calls[0]?.[1]).toMatchObject({
source: "browser",
meetingUri: "https://meet.google.com/browser-made-url",
joined: false,
browser: { nodeId: "node-1", targetId: "tab-1" },
});
expect(nodesInvoke).toHaveBeenCalledWith(
@@ -880,6 +881,101 @@ describe("google-meet plugin", () => {
);
});
it("creates and joins a Meet through the create tool action by default", async () => {
const { tools, nodesInvoke } = setup(
{
defaultTransport: "chrome-node",
defaultMode: "transcribe",
chromeNode: { node: "parallels-macos" },
},
{
nodesInvokeHandler: async (params) => {
if (params.command === "googlemeet.chrome") {
return { payload: { launched: true } };
}
const proxy = params.params as {
path?: string;
body?: { url?: string; targetId?: string; fn?: string };
};
if (proxy.path === "/tabs") {
return { payload: { result: { tabs: [] } } };
}
if (proxy.path === "/tabs/open") {
return {
payload: {
result: {
targetId:
proxy.body?.url === "https://meet.google.com/new" ? "create-tab" : "join-tab",
title: "Meet",
url: proxy.body?.url,
},
},
};
}
if (proxy.path === "/act" && proxy.body?.fn?.includes("meetUrlPattern")) {
return {
payload: {
result: {
ok: true,
targetId: "create-tab",
result: {
meetingUri: "https://meet.google.com/new-abcd-xyz",
browserUrl: "https://meet.google.com/new-abcd-xyz",
browserTitle: "Meet",
},
},
},
};
}
if (proxy.path === "/act") {
return {
payload: {
result: {
ok: true,
targetId: "join-tab",
result: JSON.stringify({
inCall: true,
micMuted: false,
title: "Meet call",
url: "https://meet.google.com/new-abcd-xyz",
}),
},
},
};
}
return { payload: { result: { ok: true } } };
},
},
);
const tool = tools[0] as {
execute: (
id: string,
params: unknown,
) => Promise<{
details: { joined?: boolean; meetingUri?: string; join?: { session: { url: string } } };
}>;
};
const result = await tool.execute("id", { action: "create" });
expect(result.details).toMatchObject({
source: "browser",
joined: true,
meetingUri: "https://meet.google.com/new-abcd-xyz",
join: { session: { url: "https://meet.google.com/new-abcd-xyz" } },
});
expect(nodesInvoke).toHaveBeenCalledWith(
expect.objectContaining({
command: "googlemeet.chrome",
params: expect.objectContaining({
action: "start",
url: "https://meet.google.com/new-abcd-xyz",
launch: false,
}),
}),
);
});
it("reuses an existing browser create tab instead of opening duplicates", async () => {
const { methods, nodesInvoke } = setup(
{
@@ -934,7 +1030,7 @@ describe("google-meet plugin", () => {
| undefined;
const respond = vi.fn();
await handler?.({ params: {}, respond });
await handler?.({ params: { join: false }, respond });
expect(respond.mock.calls[0]?.[0]).toBe(true);
expect(respond.mock.calls[0]?.[1]).toMatchObject({

View File

@@ -148,8 +148,14 @@ const GoogleMeetToolSchema = Type.Object({
"speak",
"test_speech",
],
description: "Google Meet action to run",
description:
"Google Meet action to run. create creates a meeting and joins it by default; pass join=false to only mint a meeting URL.",
}),
join: Type.Optional(
Type.Boolean({
description: "For action=create, set false to create the URL without joining.",
}),
),
url: Type.Optional(Type.String({ description: "Explicit https://meet.google.com/... URL" })),
transport: Type.Optional(
Type.String({ enum: ["chrome", "chrome-node", "twilio"], description: "Join transport" }),
@@ -240,6 +246,10 @@ function hasGoogleMeetOAuth(config: GoogleMeetConfig, raw: Record<string, unknow
);
}
function shouldJoinCreatedMeet(raw: Record<string, unknown>): boolean {
return raw.join !== false && raw.join !== "false";
}
async function createMeetFromParams(params: {
config: GoogleMeetConfig;
runtime: OpenClawPluginApi["runtime"];
@@ -247,7 +257,12 @@ async function createMeetFromParams(params: {
}) {
if (hasGoogleMeetOAuth(params.config, params.raw)) {
const { token: _token, ...result } = await createSpaceFromParams(params.config, params.raw);
return result;
return {
...result,
joined: false,
nextAction:
"URL-only creation was requested. Call google_meet with action=join and url=meetingUri to enter the meeting.",
};
}
const browser = await createMeetWithBrowserProxyOnNode({
runtime: params.runtime,
@@ -256,6 +271,9 @@ async function createMeetFromParams(params: {
return {
source: browser.source,
meetingUri: browser.meetingUri,
joined: false,
nextAction:
"URL-only creation was requested. Call google_meet with action=join and url=meetingUri to enter the meeting.",
space: {
name: `browser/${browser.meetingUri.split("/").pop()}`,
meetingUri: browser.meetingUri,
@@ -270,6 +288,31 @@ async function createMeetFromParams(params: {
};
}
async function createAndJoinMeetFromParams(params: {
config: GoogleMeetConfig;
runtime: OpenClawPluginApi["runtime"];
raw: Record<string, unknown>;
ensureRuntime: () => Promise<GoogleMeetRuntime>;
}) {
const created = await createMeetFromParams(params);
const rt = await params.ensureRuntime();
const join = await rt.join({
url: created.meetingUri,
transport: normalizeTransport(params.raw.transport),
mode: normalizeMode(params.raw.mode),
dialInNumber: normalizeOptionalString(params.raw.dialInNumber),
pin: normalizeOptionalString(params.raw.pin),
dtmfSequence: normalizeOptionalString(params.raw.dtmfSequence),
message: normalizeOptionalString(params.raw.message),
});
return {
...created,
joined: true,
nextAction: "Share meetingUri with participants; the OpenClaw agent has started the join flow.",
join,
};
}
export default definePluginEntry({
id: "google-meet",
name: "Google Meet",
@@ -324,7 +367,17 @@ export default definePluginEntry({
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = asParamRecord(params);
respond(true, await createMeetFromParams({ config, runtime: api.runtime, raw }));
respond(
true,
shouldJoinCreatedMeet(raw)
? await createAndJoinMeetFromParams({
config,
runtime: api.runtime,
raw,
ensureRuntime,
})
: await createMeetFromParams({ config, runtime: api.runtime, raw }),
);
} catch (err) {
sendError(respond, err);
}
@@ -434,7 +487,16 @@ export default definePluginEntry({
);
}
case "create": {
return json(await createMeetFromParams({ config, runtime: api.runtime, raw }));
return json(
shouldJoinCreatedMeet(raw)
? await createAndJoinMeetFromParams({
config,
runtime: api.runtime,
raw,
ensureRuntime,
})
: await createMeetFromParams({ config, runtime: api.runtime, raw }),
);
}
case "test_speech": {
const rt = await ensureRuntime();

View File

@@ -54,6 +54,13 @@ type CreateOptions = {
clientId?: string;
clientSecret?: string;
expiresAt?: string;
join?: boolean;
transport?: GoogleMeetTransport;
mode?: GoogleMeetMode;
message?: string;
dialInNumber?: string;
pin?: string;
dtmfSequence?: string;
json?: boolean;
};
@@ -233,14 +240,38 @@ export function registerGoogleMeetCli(params: {
.option("--client-id <id>", "OAuth client id override")
.option("--client-secret <secret>", "OAuth client secret override")
.option("--expires-at <ms>", "Cached access token expiry as unix epoch milliseconds")
.option("--no-join", "Only create the meeting URL; do not join it")
.option("--transport <transport>", "Join transport: chrome, chrome-node, or twilio")
.option(
"--mode <mode>",
"Join mode: realtime for live talk-back, transcribe for observe/control",
)
.option("--message <text>", "Realtime speech to trigger after join")
.option("--dial-in-number <phone>", "Meet dial-in number for Twilio transport")
.option("--pin <pin>", "Meet phone PIN; # is appended if omitted")
.option("--dtmf-sequence <sequence>", "Explicit Twilio DTMF sequence")
.option("--json", "Print JSON output", false)
.action(async (options: CreateOptions) => {
if (!hasCreateOAuth(params.config, options)) {
const rt = await params.ensureRuntime();
const result = await rt.createViaBrowser();
const join =
options.join !== false
? await rt.join({
url: result.meetingUri,
transport: options.transport,
mode: options.mode,
message: options.message,
dialInNumber: options.dialInNumber,
pin: options.pin,
dtmfSequence: options.dtmfSequence,
})
: undefined;
const payload = {
source: result.source,
meetingUri: result.meetingUri,
joined: Boolean(join),
...(join ? { join } : {}),
browser: {
nodeId: result.nodeId,
targetId: result.targetId,
@@ -255,16 +286,37 @@ export function registerGoogleMeetCli(params: {
writeStdoutLine("meeting uri: %s", result.meetingUri);
writeStdoutLine("source: browser");
writeStdoutLine("node: %s", result.nodeId);
if (join) {
writeStdoutLine("joined: %s", join.session.id);
} else {
writeStdoutLine("joined: no (run `openclaw googlemeet join %s`)", result.meetingUri);
}
return;
}
const token = await resolveGoogleMeetAccessToken(
resolveCreateTokenOptions(params.config, options),
);
const result = await createGoogleMeetSpace({ accessToken: token.accessToken });
const join =
options.join !== false
? await (
await params.ensureRuntime()
).join({
url: result.meetingUri,
transport: options.transport,
mode: options.mode,
message: options.message,
dialInNumber: options.dialInNumber,
pin: options.pin,
dtmfSequence: options.dtmfSequence,
})
: undefined;
if (options.json) {
writeStdoutJson({
...result,
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
joined: Boolean(join),
...(join ? { join } : {}),
});
return;
}
@@ -277,6 +329,11 @@ export function registerGoogleMeetCli(params: {
"token source: %s",
token.refreshed ? "refresh-token" : "cached-access-token",
);
if (join) {
writeStdoutLine("joined: %s", join.session.id);
} else {
writeStdoutLine("joined: no (run `openclaw googlemeet join %s`)", result.meetingUri);
}
});
root