From 52cc1ebac792c5063e5fbf0e4ffa39036a84072c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 02:17:09 +0100 Subject: [PATCH] fix(google-meet): surface chrome node readiness in setup --- CHANGELOG.md | 1 + docs/plugins/google-meet.md | 38 +++++++++++++++++++ extensions/google-meet/index.test.ts | 37 ++++++++++++++++-- extensions/google-meet/index.ts | 4 +- extensions/google-meet/src/cli.ts | 4 +- extensions/google-meet/src/runtime.ts | 33 ++++++++++++++-- extensions/google-meet/src/setup.ts | 18 ++++++++- .../src/transports/chrome-browser-proxy.ts | 25 +++++++++--- 8 files changed, 142 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8164fcb3e38..fd4f0b69f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/Google Meet: include live Chrome-node readiness in `googlemeet setup` and document the Parallels recovery checks, so stale node tokens or disconnected VM browsers are visible before an agent opens a meeting. Thanks @steipete. - Plugins/runtime deps: isolate the internal npm cache used for bundled plugin runtime-dependency repair and let package updates refresh/verify already-current installs, so failed update or sudo doctor runs can be repaired by rerunning `openclaw update`. Thanks @steipete. - Plugins/runtime deps: stage bundled plugin runtime dependencies for packaged/global installs in an external runtime root and retain already staged deps across repairs, avoiding package-tree update races and npm pruning after upgrades. Thanks @steipete. - Plugins/runtime deps: log bundled plugin runtime-dependency staging before synchronous npm installs start and include elapsed timing afterward, so first boot after upgrades no longer looks hung while dependencies are being repaired. Thanks @steipete. diff --git a/docs/plugins/google-meet.md b/docs/plugins/google-meet.md index d9b6fb48017..05447c0401b 100644 --- a/docs/plugins/google-meet.md +++ b/docs/plugins/google-meet.md @@ -728,11 +728,29 @@ openclaw googlemeet test-speech https://meet.google.com/abc-defg-hij \ Expected Chrome-node state: - `googlemeet setup` is all green. +- `googlemeet setup` includes `chrome-node-connected` when Chrome-node is the + default transport or a node is pinned. - `nodes status` shows the selected node connected. - The selected node advertises both `googlemeet.chrome` and `browser.proxy`. - The Meet tab joins the call and `test-speech` returns Chrome health with `inCall: true`. +For a remote Chrome host such as a Parallels macOS VM, this is the shortest +safe check after updating the Gateway or the VM: + +```bash +openclaw googlemeet setup +openclaw nodes status --connected +openclaw nodes invoke \ + --node parallels-macos \ + --command googlemeet.chrome \ + --params '{"action":"setup"}' +``` + +That proves the Gateway plugin is loaded, the VM node is connected with the +current token, and the Meet audio bridge is available before an agent opens a +real meeting tab. + For a Twilio smoke, use a meeting that exposes phone dial-in details: ```bash @@ -798,6 +816,26 @@ The Gateway config must allow those node commands: } ``` +If `googlemeet setup` fails `chrome-node-connected` or the Gateway log reports +`gateway token mismatch`, reinstall or restart the node with the current Gateway +token. For a LAN Gateway this usually means: + +```bash +OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1 \ + openclaw node install \ + --host \ + --port 18789 \ + --display-name parallels-macos \ + --force +``` + +Then reload the node service and re-run: + +```bash +openclaw googlemeet setup +openclaw nodes status --connected +``` + ### Browser opens but agent cannot join Run `googlemeet test-speech` and inspect the returned Chrome health. If it diff --git a/extensions/google-meet/index.test.ts b/extensions/google-meet/index.test.ts index 9a2d5e2e248..ca8893db76f 100644 --- a/extensions/google-meet/index.test.ts +++ b/extensions/google-meet/index.test.ts @@ -451,6 +451,35 @@ describe("google-meet plugin", () => { expect(result.details.ok).toBe(true); }); + it("fails setup status when the configured Chrome node is not connected", async () => { + const { tools } = setup( + { + defaultTransport: "chrome-node", + chromeNode: { node: "parallels-macos" }, + }, + { nodesListResult: { nodes: [] } }, + ); + const tool = tools[0] as { + execute: ( + id: string, + params: unknown, + ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>; + }; + + const result = await tool.execute("id", { action: "setup_status" }); + + expect(result.details.ok).toBe(false); + expect(result.details.checks).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "chrome-node-connected", + ok: false, + message: expect.stringContaining("No connected Google Meet-capable node"), + }), + ]), + ); + }); + it("reports Twilio delegation readiness when voice-call is enabled", async () => { vi.stubEnv("TWILIO_ACCOUNT_SID", "AC123"); vi.stubEnv("TWILIO_AUTH_TOKEN", "secret"); @@ -547,7 +576,7 @@ describe("google-meet plugin", () => { config: resolveGoogleMeetConfig({}), ensureRuntime: async () => ({ - setupStatus: () => ({ + setupStatus: async () => ({ ok: true, checks: [ { @@ -557,7 +586,7 @@ describe("google-meet plugin", () => { }, ], }), - }) as GoogleMeetRuntime, + }) as unknown as GoogleMeetRuntime, }); try { @@ -580,11 +609,11 @@ describe("google-meet plugin", () => { config: resolveGoogleMeetConfig({}), ensureRuntime: async () => ({ - setupStatus: () => ({ + setupStatus: async () => ({ ok: false, checks: [{ id: "twilio-voice-call-plugin", ok: false, message: "missing" }], }), - }) as GoogleMeetRuntime, + }) as unknown as GoogleMeetRuntime, }); try { diff --git a/extensions/google-meet/index.ts b/extensions/google-meet/index.ts index 7c9a19f1bf9..a65a817631e 100644 --- a/extensions/google-meet/index.ts +++ b/extensions/google-meet/index.ts @@ -313,7 +313,7 @@ export default definePluginEntry({ async ({ respond }: GatewayRequestHandlerOptions) => { try { const rt = await ensureRuntime(); - respond(true, rt.setupStatus()); + respond(true, await rt.setupStatus()); } catch (err) { sendError(respond, err); } @@ -430,7 +430,7 @@ export default definePluginEntry({ } case "setup_status": { const rt = await ensureRuntime(); - return json(rt.setupStatus()); + return json(await rt.setupStatus()); } case "resolve_space": { const { token: _token, ...result } = await resolveSpaceFromParams(config, raw); diff --git a/extensions/google-meet/src/cli.ts b/extensions/google-meet/src/cli.ts index 8a168de7391..e55c2e14718 100644 --- a/extensions/google-meet/src/cli.ts +++ b/extensions/google-meet/src/cli.ts @@ -95,7 +95,7 @@ function parseOptionalNumber(value: string | undefined): number | undefined { return parsed; } -function writeSetupStatus(status: ReturnType): void { +function writeSetupStatus(status: Awaited>): void { writeStdoutLine("Google Meet setup: %s", status.ok ? "OK" : "needs attention"); for (const check of status.checks) { writeStdoutLine("[%s] %s: %s", check.ok ? "ok" : "fail", check.id, check.message); @@ -485,7 +485,7 @@ export function registerGoogleMeetCli(params: { .option("--json", "Print JSON output", false) .action(async (options: SetupOptions) => { const rt = await params.ensureRuntime(); - const status = rt.setupStatus(); + const status = await rt.setupStatus(); if (options.json) { writeStdoutJson(status); return; diff --git a/extensions/google-meet/src/runtime.ts b/extensions/google-meet/src/runtime.ts index ae3d44350ce..9dcc7939953 100644 --- a/extensions/google-meet/src/runtime.ts +++ b/extensions/google-meet/src/runtime.ts @@ -4,7 +4,8 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk/plugin-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import type { GoogleMeetConfig, GoogleMeetMode, GoogleMeetTransport } from "./config.js"; -import { getGoogleMeetSetupStatus } from "./setup.js"; +import { addGoogleMeetSetupCheck, getGoogleMeetSetupStatus } from "./setup.js"; +import { resolveChromeNodeInfo } from "./transports/chrome-browser-proxy.js"; import { createMeetWithBrowserProxyOnNode } from "./transports/chrome-create.js"; import { launchChromeMeet, launchChromeMeetOnNode } from "./transports/chrome.js"; import { buildMeetDtmfSequence, normalizeDialInNumber } from "./transports/twilio.js"; @@ -81,8 +82,34 @@ export class GoogleMeetRuntime { return session ? { found: true, session } : { found: false }; } - setupStatus() { - return getGoogleMeetSetupStatus(this.params.config, { fullConfig: this.params.fullConfig }); + async setupStatus() { + let status = getGoogleMeetSetupStatus(this.params.config, { + fullConfig: this.params.fullConfig, + }); + if ( + this.params.config.defaultTransport === "chrome-node" || + Boolean(this.params.config.chromeNode.node) + ) { + try { + const node = await resolveChromeNodeInfo({ + runtime: this.params.runtime, + requestedNode: this.params.config.chromeNode.node, + }); + const label = node.displayName ?? node.remoteIp ?? node.nodeId ?? "connected node"; + status = addGoogleMeetSetupCheck(status, { + id: "chrome-node-connected", + ok: true, + message: `Connected Google Meet node ready: ${label}`, + }); + } catch (error) { + status = addGoogleMeetSetupCheck(status, { + id: "chrome-node-connected", + ok: false, + message: formatErrorMessage(error), + }); + } + } + return status; } async createViaBrowser() { diff --git a/extensions/google-meet/src/setup.ts b/extensions/google-meet/src/setup.ts index 816ccd3e350..f7bb65b638d 100644 --- a/extensions/google-meet/src/setup.ts +++ b/extensions/google-meet/src/setup.ts @@ -3,12 +3,17 @@ import os from "node:os"; import path from "node:path"; import type { GoogleMeetConfig } from "./config.js"; -type SetupCheck = { +export type SetupCheck = { id: string; ok: boolean; message: string; }; +export type GoogleMeetSetupStatus = { + ok: boolean; + checks: SetupCheck[]; +}; + function resolveUserPath(input: string): string { if (input === "~") { return os.homedir(); @@ -177,6 +182,17 @@ export function getGoogleMeetSetupStatus( }; } +export function addGoogleMeetSetupCheck( + status: GoogleMeetSetupStatus, + check: SetupCheck, +): GoogleMeetSetupStatus { + const checks = [...status.checks, check]; + return { + ok: checks.every((item) => item.ok), + checks, + }; +} + function asRecord(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) diff --git a/extensions/google-meet/src/transports/chrome-browser-proxy.ts b/extensions/google-meet/src/transports/chrome-browser-proxy.ts index 292ccde9026..e78ac568b1d 100644 --- a/extensions/google-meet/src/transports/chrome-browser-proxy.ts +++ b/extensions/google-meet/src/transports/chrome-browser-proxy.ts @@ -10,14 +10,16 @@ export type BrowserTab = { url?: string; }; -function isGoogleMeetNode(node: { +export type GoogleMeetNodeInfo = { caps?: string[]; commands?: string[]; connected?: boolean; nodeId?: string; displayName?: string; remoteIp?: string; -}) { +}; + +function isGoogleMeetNode(node: GoogleMeetNodeInfo) { const commands = Array.isArray(node.commands) ? node.commands : []; const caps = Array.isArray(node.caps) ? node.caps : []; return ( @@ -27,10 +29,10 @@ function isGoogleMeetNode(node: { ); } -export async function resolveChromeNode(params: { +export async function resolveChromeNodeInfo(params: { runtime: PluginRuntime; requestedNode?: string; -}): Promise { +}): Promise { const list = await params.runtime.nodes.list({ connected: true }); const nodes = list.nodes.filter(isGoogleMeetNode); if (nodes.length === 0) { @@ -44,18 +46,29 @@ export async function resolveChromeNode(params: { [node.nodeId, node.displayName, node.remoteIp].some((value) => value === requested), ); if (matches.length === 1) { - return matches[0].nodeId; + return matches[0]; } throw new Error(`Google Meet node not found or ambiguous: ${requested}`); } if (nodes.length === 1) { - return nodes[0].nodeId; + return nodes[0]; } throw new Error( "Multiple Google Meet-capable nodes connected. Set plugins.entries.google-meet.config.chromeNode.node.", ); } +export async function resolveChromeNode(params: { + runtime: PluginRuntime; + requestedNode?: string; +}): Promise { + const node = await resolveChromeNodeInfo(params); + if (!node.nodeId) { + throw new Error("Google Meet node did not include a node id."); + } + return node.nodeId; +} + function unwrapNodeInvokePayload(raw: unknown): unknown { const record = raw && typeof raw === "object" ? (raw as Record) : {}; if (typeof record.payloadJSON === "string" && record.payloadJSON.trim()) {