mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(google-meet): join created meetings by default
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user