mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:31:00 +00:00
fix(google-meet): use OpenClaw browser for local joins
This commit is contained in:
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Google Meet: route local Chrome joins through OpenClaw browser control instead of raw default Chrome, so agents use the configured OpenClaw browser profile when opening Meet. Thanks @openclaw.
|
||||
- Reply/link understanding: keep media and link preprocessing on stable runtime entrypoints and continue with raw message content if optional enrichment fails, so URL-bearing messages are no longer dropped after stale runtime chunk upgrades. Fixes #68466. Thanks @songshikang0111.
|
||||
- Discord: persist routed model-picker overrides when the hidden `/model` dispatch succeeds but the bound thread session store is still stale, including LM Studio suffixed model ids. Carries forward #61473. Thanks @Nanako0129.
|
||||
- Nodes/CLI: add `openclaw nodes remove --node <id|name|ip>` and `node.pair.remove` so stale gateway-owned node pairing records can be cleaned without hand-editing state files. Thanks @openclaw.
|
||||
|
||||
@@ -155,10 +155,10 @@ appears, browser automation handles it when it can. Login, host admission, and
|
||||
browser/OS permission prompts are reported as manual action with a reason and
|
||||
message for the agent to relay.
|
||||
|
||||
Chrome joins as the signed-in Chrome profile. In Meet, pick `BlackHole 2ch` for
|
||||
the microphone/speaker path used by OpenClaw. For clean duplex audio, use
|
||||
separate virtual devices or a Loopback-style graph; a single BlackHole device is
|
||||
enough for a first smoke test but can echo.
|
||||
Local Chrome joins through the signed-in OpenClaw browser profile. In Meet, pick
|
||||
`BlackHole 2ch` for the microphone/speaker path used by OpenClaw. For clean
|
||||
duplex audio, use separate virtual devices or a Loopback-style graph; a single
|
||||
BlackHole device is enough for a first smoke test but can echo.
|
||||
|
||||
### Local gateway + Parallels Chrome
|
||||
|
||||
@@ -350,12 +350,14 @@ upstream licensing terms or get a separate license from Existential Audio.
|
||||
|
||||
### Chrome
|
||||
|
||||
Chrome transport opens the Meet URL in Google Chrome and joins as the signed-in
|
||||
Chrome profile. On macOS, the plugin checks for `BlackHole 2ch` before launch.
|
||||
If configured, it also runs an audio bridge health command and startup command
|
||||
before opening Chrome. Use `chrome` when Chrome/audio live on the Gateway host;
|
||||
use `chrome-node` when Chrome/audio live on a paired node such as a Parallels
|
||||
macOS VM.
|
||||
Chrome transport opens the Meet URL through OpenClaw browser control and joins
|
||||
as the signed-in OpenClaw browser profile. On macOS, the plugin checks for
|
||||
`BlackHole 2ch` before launch. If configured, it also runs an audio bridge
|
||||
health command and startup command before opening Chrome. Use `chrome` when
|
||||
Chrome/audio live on the Gateway host; use `chrome-node` when Chrome/audio live
|
||||
on a paired node such as a Parallels macOS VM. For local Chrome, choose the
|
||||
profile with `browser.defaultProfile`; `chrome.browserProfile` is passed to
|
||||
`chrome-node` hosts.
|
||||
|
||||
```bash
|
||||
openclaw googlemeet join https://meet.google.com/abc-defg-hij --transport chrome
|
||||
@@ -910,8 +912,10 @@ Optional overrides:
|
||||
defaults: {
|
||||
meeting: "https://meet.google.com/abc-defg-hij",
|
||||
},
|
||||
browser: {
|
||||
defaultProfile: "openclaw",
|
||||
},
|
||||
chrome: {
|
||||
browserProfile: "Default",
|
||||
guestName: "OpenClaw Agent",
|
||||
waitForInCallMs: 30000,
|
||||
},
|
||||
|
||||
@@ -94,6 +94,45 @@ function requestUrl(input: RequestInfo | URL): URL {
|
||||
return new URL(input.url);
|
||||
}
|
||||
|
||||
function mockLocalMeetBrowserRequest(
|
||||
browserActResult: Record<string, unknown> = {
|
||||
inCall: true,
|
||||
micMuted: false,
|
||||
title: "Meet call",
|
||||
url: "https://meet.google.com/abc-defg-hij",
|
||||
},
|
||||
) {
|
||||
const callGatewayFromCli = vi.fn(
|
||||
async (
|
||||
_method: string,
|
||||
_opts: unknown,
|
||||
params?: unknown,
|
||||
_extra?: unknown,
|
||||
): Promise<Record<string, unknown>> => {
|
||||
const request = params as { path?: string; body?: { targetId?: string; url?: string } };
|
||||
if (request.path === "/tabs") {
|
||||
return { tabs: [] };
|
||||
}
|
||||
if (request.path === "/tabs/open") {
|
||||
return {
|
||||
targetId: "local-meet-tab",
|
||||
title: "Meet",
|
||||
url: request.body?.url ?? "https://meet.google.com/abc-defg-hij",
|
||||
};
|
||||
}
|
||||
if (request.path === "/tabs/focus") {
|
||||
return { ok: true };
|
||||
}
|
||||
if (request.path === "/act") {
|
||||
return { result: JSON.stringify(browserActResult) };
|
||||
}
|
||||
throw new Error(`unexpected browser request path ${request.path}`);
|
||||
},
|
||||
);
|
||||
chromeTransportTesting.setDepsForTest({ callGatewayFromCli });
|
||||
return callGatewayFromCli;
|
||||
}
|
||||
|
||||
function stubMeetArtifactsApi() {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = requestUrl(input);
|
||||
@@ -1332,13 +1371,14 @@ describe("google-meet plugin", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("launches Chrome after the BlackHole check", async () => {
|
||||
it("opens local Chrome Meet through browser control after the BlackHole check", async () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, "platform", { value: "darwin" });
|
||||
try {
|
||||
const { methods, runCommandWithTimeout } = setup({
|
||||
defaultMode: "transcribe",
|
||||
});
|
||||
const callGatewayFromCli = mockLocalMeetBrowserRequest();
|
||||
const handler = methods.get("googlemeet.join") as
|
||||
| ((ctx: {
|
||||
params: Record<string, unknown>;
|
||||
@@ -1358,10 +1398,16 @@ describe("google-meet plugin", () => {
|
||||
["/usr/sbin/system_profiler", "SPAudioDataType"],
|
||||
{ timeoutMs: 10000 },
|
||||
);
|
||||
expect(runCommandWithTimeout).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
["open", "-a", "Google Chrome", "https://meet.google.com/abc-defg-hij"],
|
||||
{ timeoutMs: 30000 },
|
||||
expect(runCommandWithTimeout).toHaveBeenCalledTimes(1);
|
||||
expect(callGatewayFromCli).toHaveBeenCalledWith(
|
||||
"browser.request",
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
path: "/tabs/open",
|
||||
body: { url: "https://meet.google.com/abc-defg-hij" },
|
||||
}),
|
||||
{ progress: false },
|
||||
);
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||
@@ -1919,6 +1965,7 @@ describe("google-meet plugin", () => {
|
||||
audioBridgeCommand: ["bridge", "start"],
|
||||
},
|
||||
});
|
||||
const callGatewayFromCli = mockLocalMeetBrowserRequest();
|
||||
const handler = methods.get("googlemeet.join") as
|
||||
| ((ctx: {
|
||||
params: Record<string, unknown>;
|
||||
@@ -1939,6 +1986,16 @@ describe("google-meet plugin", () => {
|
||||
expect(runCommandWithTimeout).toHaveBeenNthCalledWith(3, ["bridge", "start"], {
|
||||
timeoutMs: 30000,
|
||||
});
|
||||
expect(callGatewayFromCli).toHaveBeenCalledWith(
|
||||
"browser.request",
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
path: "/tabs/open",
|
||||
body: { url: "https://meet.google.com/abc-defg-hij" },
|
||||
}),
|
||||
{ progress: false },
|
||||
);
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||
}
|
||||
|
||||
@@ -71,29 +71,13 @@ export function getGoogleMeetSetupStatus(
|
||||
});
|
||||
}
|
||||
|
||||
if (config.chrome.browserProfile) {
|
||||
const profilePath = path.join(
|
||||
os.homedir(),
|
||||
"Library",
|
||||
"Application Support",
|
||||
"Google",
|
||||
"Chrome",
|
||||
config.chrome.browserProfile,
|
||||
);
|
||||
checks.push({
|
||||
id: "chrome-profile",
|
||||
ok: fs.existsSync(profilePath),
|
||||
message: fs.existsSync(profilePath)
|
||||
? "Chrome profile found"
|
||||
: `Chrome profile missing: ${config.chrome.browserProfile}`,
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
id: "chrome-profile",
|
||||
ok: true,
|
||||
message: "Chrome profile not pinned; default signed-in profile will be used",
|
||||
});
|
||||
}
|
||||
checks.push({
|
||||
id: "chrome-profile",
|
||||
ok: true,
|
||||
message: config.chrome.browserProfile
|
||||
? "Local Chrome uses the OpenClaw browser profile; chrome.browserProfile is passed to chrome-node hosts"
|
||||
: "Local Chrome uses the OpenClaw browser profile; configure browser.defaultProfile to choose another profile",
|
||||
});
|
||||
|
||||
checks.push({
|
||||
id: "audio-bridge",
|
||||
|
||||
@@ -93,6 +93,7 @@ export async function launchChromeMeet(params: {
|
||||
audioBridge?:
|
||||
| { type: "external-command" }
|
||||
| ({ type: "command-pair" } & ChromeRealtimeAudioBridgeHandle);
|
||||
browser?: GoogleMeetChromeHealth;
|
||||
}> {
|
||||
await assertBlackHole2chAvailable({
|
||||
runtime: params.runtime,
|
||||
@@ -151,12 +152,6 @@ export async function launchChromeMeet(params: {
|
||||
return { launched: false, audioBridge };
|
||||
}
|
||||
|
||||
const argv = ["open", "-a", "Google Chrome"];
|
||||
if (params.config.chrome.browserProfile) {
|
||||
argv.push("--args", `--profile-directory=${params.config.chrome.browserProfile}`);
|
||||
}
|
||||
argv.push(params.url);
|
||||
|
||||
let commandPairBridgeStopped = false;
|
||||
const stopCommandPairBridge = async () => {
|
||||
if (commandPairBridgeStopped) {
|
||||
@@ -169,16 +164,12 @@ export async function launchChromeMeet(params: {
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await params.runtime.system.runCommandWithTimeout(argv, {
|
||||
timeoutMs: params.config.chrome.joinTimeoutMs,
|
||||
const result = await openMeetWithBrowserRequest({
|
||||
callBrowser: callLocalBrowserRequest,
|
||||
config: params.config,
|
||||
url: params.url,
|
||||
});
|
||||
if (result.code === 0) {
|
||||
return { launched: true, audioBridge };
|
||||
}
|
||||
await stopCommandPairBridge();
|
||||
throw new Error(
|
||||
`failed to launch Chrome for Meet: ${result.stderr || result.stdout || result.code}`,
|
||||
);
|
||||
return { ...result, audioBridge };
|
||||
} catch (error) {
|
||||
await stopCommandPairBridge();
|
||||
throw error;
|
||||
@@ -328,6 +319,26 @@ async function openMeetWithBrowserProxy(params: {
|
||||
nodeId: string;
|
||||
config: GoogleMeetConfig;
|
||||
url: string;
|
||||
}): Promise<{ launched: boolean; browser?: GoogleMeetChromeHealth }> {
|
||||
return await openMeetWithBrowserRequest({
|
||||
callBrowser: async (request) =>
|
||||
await callBrowserProxyOnNode({
|
||||
runtime: params.runtime,
|
||||
nodeId: params.nodeId,
|
||||
method: request.method,
|
||||
path: request.path,
|
||||
body: request.body,
|
||||
timeoutMs: request.timeoutMs,
|
||||
}),
|
||||
config: params.config,
|
||||
url: params.url,
|
||||
});
|
||||
}
|
||||
|
||||
async function openMeetWithBrowserRequest(params: {
|
||||
callBrowser: BrowserRequestCaller;
|
||||
config: GoogleMeetConfig;
|
||||
url: string;
|
||||
}): Promise<{ launched: boolean; browser?: GoogleMeetChromeHealth }> {
|
||||
if (!params.config.chrome.launch) {
|
||||
return { launched: false };
|
||||
@@ -338,9 +349,7 @@ async function openMeetWithBrowserProxy(params: {
|
||||
let tab: BrowserTab | undefined;
|
||||
if (params.config.chrome.reuseExistingTab) {
|
||||
const tabs = asBrowserTabs(
|
||||
await callBrowserProxyOnNode({
|
||||
runtime: params.runtime,
|
||||
nodeId: params.nodeId,
|
||||
await params.callBrowser({
|
||||
method: "GET",
|
||||
path: "/tabs",
|
||||
timeoutMs: Math.min(timeoutMs, 5_000),
|
||||
@@ -349,9 +358,7 @@ async function openMeetWithBrowserProxy(params: {
|
||||
tab = tabs.find((entry) => isSameMeetUrlForReuse(entry.url, params.url));
|
||||
targetId = tab?.targetId;
|
||||
if (targetId) {
|
||||
await callBrowserProxyOnNode({
|
||||
runtime: params.runtime,
|
||||
nodeId: params.nodeId,
|
||||
await params.callBrowser({
|
||||
method: "POST",
|
||||
path: "/tabs/focus",
|
||||
body: { targetId },
|
||||
@@ -361,9 +368,7 @@ async function openMeetWithBrowserProxy(params: {
|
||||
}
|
||||
if (!targetId) {
|
||||
tab = readBrowserTab(
|
||||
await callBrowserProxyOnNode({
|
||||
runtime: params.runtime,
|
||||
nodeId: params.nodeId,
|
||||
await params.callBrowser({
|
||||
method: "POST",
|
||||
path: "/tabs/open",
|
||||
body: { url: params.url },
|
||||
@@ -392,9 +397,7 @@ async function openMeetWithBrowserProxy(params: {
|
||||
};
|
||||
do {
|
||||
try {
|
||||
const evaluated = await callBrowserProxyOnNode({
|
||||
runtime: params.runtime,
|
||||
nodeId: params.nodeId,
|
||||
const evaluated = await params.callBrowser({
|
||||
method: "POST",
|
||||
path: "/act",
|
||||
body: {
|
||||
|
||||
Reference in New Issue
Block a user