mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:40:44 +00:00
fix(google-meet): join created meetings by default
This commit is contained in:
@@ -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