@openclaw/google-meet 2026.7.2-beta.7 → 2026.8.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chrome-create-PKAyxCiL.js +325 -0
- package/dist/{cli-DCcRyaMf.js → cli-Bd63VJ5q.js} +191 -237
- package/dist/{config-BcaS1BDr.js → config-wBK8loBb.js} +97 -221
- package/dist/{create-af1MDlus.js → create-BEDHM6uL.js} +3 -14
- package/dist/google-meet-platform-adapter-C3J-iFRE.js +791 -0
- package/dist/google-meet-platform-constants-Bs5iAg3E.js +25 -0
- package/dist/index.js +185 -2639
- package/dist/meet-url-BP2Jlvjs.js +17 -0
- package/dist/node-host-BXtXbsMa.js +49 -0
- package/dist/node-invoke-policy-B0gOlLJR.js +24 -0
- package/dist/{oauth-DOfMKlGp.js → oauth-9sUJ68Mw.js} +3 -14
- package/dist/{chrome-create-DMKruRFW.js → plugin-helpers-C9-Y0Ago.js} +335 -355
- package/dist/plugin-registration-Csi-NdQU.js +131 -0
- package/dist/runtime-B8d1beHo.js +1308 -0
- package/openclaw.plugin.json +15 -47
- package/package.json +4 -6
- package/node_modules/pretty-ms/index.d.ts +0 -157
- package/node_modules/pretty-ms/index.js +0 -149
- package/node_modules/pretty-ms/license +0 -9
- package/node_modules/pretty-ms/node_modules/parse-ms/index.d.ts +0 -30
- package/node_modules/pretty-ms/node_modules/parse-ms/index.js +0 -45
- package/node_modules/pretty-ms/node_modules/parse-ms/license +0 -9
- package/node_modules/pretty-ms/node_modules/parse-ms/package.json +0 -47
- package/node_modules/pretty-ms/node_modules/parse-ms/readme.md +0 -46
- package/node_modules/pretty-ms/package.json +0 -55
- package/node_modules/pretty-ms/readme.md +0 -179
|
@@ -0,0 +1,1308 @@
|
|
|
1
|
+
import { n as GOOGLE_MEET_NODE_COMMAND } from "./google-meet-platform-constants-Bs5iAg3E.js";
|
|
2
|
+
import { t as normalizeMeetUrl } from "./meet-url-BP2Jlvjs.js";
|
|
3
|
+
import { c as callBrowserProxyOnNode, l as resolveChromeNode, u as resolveChromeNodeInfo } from "./chrome-create-PKAyxCiL.js";
|
|
4
|
+
import { n as normalizeDialInNumber, t as GOOGLE_MEET_PLATFORM_ADAPTER } from "./google-meet-platform-adapter-C3J-iFRE.js";
|
|
5
|
+
import { asOptionalRecord, asRecord, normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
|
+
import { addTimerTimeoutGraceMs } from "openclaw/plugin-sdk/number-runtime";
|
|
8
|
+
import { GatewayClient, startGatewayClientWhenEventLoopReady } from "openclaw/plugin-sdk/gateway-runtime";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
11
|
+
import { MeetingPlatformAdapter, MeetingSessionRuntime, addMeetingSetupCheck, createLocalMeetingRealtimeAudioTransport, createMeetingRealtimeEngineBindings, createMeetingSession, createMeetingSetupStatus, createMeetingVoiceCallGateway, createNodeMeetingRealtimeAudioTransport, endMeetingVoiceCallGatewayCall, getMeetingVoiceCallGatewayCall, isMeetingVoiceCallMissingError, joinMeetingViaVoiceCallGateway, leaveMeetingWithBrowser, openMeetingWithBrowser, readMeetingTranscriptWithBrowser, recoverMeetingBrowserTab, resolveLocalMeetingBrowserRequest, speakMeetingViaVoiceCallGateway, startMeetingAgentRealtimeEngine, startMeetingRealtimeEngine } from "openclaw/plugin-sdk/meeting-runtime";
|
|
12
|
+
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
|
|
13
|
+
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import { resolveTranscriptsConfig } from "openclaw/plugin-sdk/transcripts";
|
|
17
|
+
//#region extensions/google-meet/src/runtime-probes.ts
|
|
18
|
+
function resolveProbeTimeoutMs(input, fallback) {
|
|
19
|
+
if (input === void 0) return Math.min(Math.max(fallback, 1), 12e4);
|
|
20
|
+
if (!Number.isFinite(input) || input <= 0) throw new Error("timeoutMs must be a positive number");
|
|
21
|
+
return Math.min(Math.trunc(input), 12e4);
|
|
22
|
+
}
|
|
23
|
+
const probes = MeetingPlatformAdapter.createRuntimeProbes({
|
|
24
|
+
defaultSpeechMessage: "Say exactly: Google Meet speech test complete.",
|
|
25
|
+
invalidRequest: (message) => new Error(message),
|
|
26
|
+
resolveTimeoutMs: resolveProbeTimeoutMs,
|
|
27
|
+
shouldWaitForListening: (session) => Boolean((session.transport === "chrome" || session.transport === "chrome-node") && session.chrome?.launched),
|
|
28
|
+
talkBackMode: MeetingPlatformAdapter.isTalkBackMode,
|
|
29
|
+
normalizeUrl: normalizeMeetUrl,
|
|
30
|
+
resolveRequestMode: (mode) => mode === "realtime" ? "agent" : mode,
|
|
31
|
+
defaultTransport: (config) => config.defaultTransport,
|
|
32
|
+
validateListeningTransport: (transport) => {
|
|
33
|
+
if (transport === "twilio") throw new Error("test_listen supports chrome or chrome-node transports");
|
|
34
|
+
},
|
|
35
|
+
resolveSpeechTimeoutMs: (_request, config) => Math.min(config.chrome.joinTimeoutMs, 5e3),
|
|
36
|
+
refreshCaptionHealth: async (context, session) => await context.refreshCaptionHealth(session),
|
|
37
|
+
speechModeError: "test_speech requires mode: agent or bidi; use join mode: transcribe for observe-only sessions.",
|
|
38
|
+
listeningModeError: "test_listen requires mode: transcribe; use test_speech for talk-back sessions."
|
|
39
|
+
});
|
|
40
|
+
const testGoogleMeetListening = probes.testListening;
|
|
41
|
+
const testGoogleMeetSpeech = probes.testSpeech;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region extensions/google-meet/src/runtime-session.ts
|
|
44
|
+
function resolveTransport(input, config) {
|
|
45
|
+
return input ?? config.defaultTransport;
|
|
46
|
+
}
|
|
47
|
+
function resolveMode(input, config) {
|
|
48
|
+
return input === "realtime" ? "agent" : input ?? config.defaultMode;
|
|
49
|
+
}
|
|
50
|
+
function withSessionAgentConfig(config, agentId) {
|
|
51
|
+
return config.realtime.agentId === agentId ? config : {
|
|
52
|
+
...config,
|
|
53
|
+
realtime: {
|
|
54
|
+
...config.realtime,
|
|
55
|
+
agentId
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function isBrowserTransport(transport) {
|
|
60
|
+
return transport === "chrome" || transport === "chrome-node";
|
|
61
|
+
}
|
|
62
|
+
function noteSession(session, note) {
|
|
63
|
+
session.notes = [...session.notes.filter((item) => item !== note), note];
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region extensions/google-meet/src/setup.ts
|
|
67
|
+
function resolveUserPath(input) {
|
|
68
|
+
if (input === "~") return os.homedir();
|
|
69
|
+
if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
|
|
70
|
+
return input;
|
|
71
|
+
}
|
|
72
|
+
function isProviderUnreachableWebhookUrl(webhookUrl) {
|
|
73
|
+
try {
|
|
74
|
+
return isBlockedHostnameOrIp(new URL(webhookUrl).hostname);
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function resolveVoiceCallSetupValue(configured, fallback) {
|
|
80
|
+
return normalizeOptionalString(configured) ?? normalizeOptionalString(fallback);
|
|
81
|
+
}
|
|
82
|
+
function getVoiceCallWebhookExposureCheck(voiceCallConfig) {
|
|
83
|
+
const publicUrl = normalizeOptionalString(voiceCallConfig.publicUrl);
|
|
84
|
+
const tunnel = asRecord(voiceCallConfig.tunnel);
|
|
85
|
+
const tailscale = asRecord(voiceCallConfig.tailscale);
|
|
86
|
+
const tunnelProvider = normalizeOptionalString(tunnel.provider);
|
|
87
|
+
const tailscaleMode = normalizeOptionalString(tailscale.mode);
|
|
88
|
+
if (publicUrl) {
|
|
89
|
+
const ok = !isProviderUnreachableWebhookUrl(publicUrl);
|
|
90
|
+
return {
|
|
91
|
+
id: "twilio-voice-call-webhook",
|
|
92
|
+
ok,
|
|
93
|
+
message: ok ? `Voice-call public webhook URL configured: ${publicUrl}` : `Voice-call publicUrl is local/private and cannot be reached by Twilio: ${publicUrl}`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (tunnelProvider && tunnelProvider !== "none") return {
|
|
97
|
+
id: "twilio-voice-call-webhook",
|
|
98
|
+
ok: true,
|
|
99
|
+
message: "Voice-call webhook exposure configured through tunnel"
|
|
100
|
+
};
|
|
101
|
+
if (tailscaleMode && tailscaleMode !== "off") return {
|
|
102
|
+
id: "twilio-voice-call-webhook",
|
|
103
|
+
ok: true,
|
|
104
|
+
message: "Voice-call webhook exposure configured through Tailscale"
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
id: "twilio-voice-call-webhook",
|
|
108
|
+
ok: false,
|
|
109
|
+
message: "Set plugins.entries.voice-call.config.publicUrl or configure voice-call tunnel/tailscale exposure for Twilio dialing"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function getGoogleMeetSetupStatus(config, options) {
|
|
113
|
+
const checks = [];
|
|
114
|
+
const env = options?.env ?? process.env;
|
|
115
|
+
const fullConfig = asRecord(options?.fullConfig);
|
|
116
|
+
const mode = options?.mode ?? config.defaultMode;
|
|
117
|
+
const transport = options?.transport ?? config.defaultTransport;
|
|
118
|
+
const needsChromeRealtimeAudio = MeetingPlatformAdapter.isTalkBackMode(mode) && (transport === "chrome" || transport === "chrome-node");
|
|
119
|
+
const pluginEntries = asRecord(asRecord(fullConfig.plugins).entries);
|
|
120
|
+
const pluginAllow = asRecord(fullConfig.plugins).allow;
|
|
121
|
+
const voiceCallEntry = asRecord(pluginEntries["voice-call"]);
|
|
122
|
+
const voiceCallConfig = asRecord(voiceCallEntry.config);
|
|
123
|
+
const voiceCallTwilioConfig = asRecord(voiceCallConfig.twilio);
|
|
124
|
+
if (config.auth.tokenPath) {
|
|
125
|
+
const tokenPath = resolveUserPath(config.auth.tokenPath);
|
|
126
|
+
checks.push({
|
|
127
|
+
id: "google-oauth-token",
|
|
128
|
+
ok: fs.existsSync(tokenPath),
|
|
129
|
+
message: fs.existsSync(tokenPath) ? "Google OAuth token file found" : `Google OAuth token file missing at ${config.auth.tokenPath}`
|
|
130
|
+
});
|
|
131
|
+
} else checks.push({
|
|
132
|
+
id: "google-oauth-token",
|
|
133
|
+
ok: true,
|
|
134
|
+
message: "Google OAuth token path not configured; Chrome profile auth will be used"
|
|
135
|
+
});
|
|
136
|
+
checks.push({
|
|
137
|
+
id: "chrome-profile",
|
|
138
|
+
ok: true,
|
|
139
|
+
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"
|
|
140
|
+
});
|
|
141
|
+
if (needsChromeRealtimeAudio) {
|
|
142
|
+
const hasCommandPair = Boolean(config.chrome.audioInputCommand && config.chrome.audioOutputCommand);
|
|
143
|
+
const hasExternalBridge = Boolean(config.chrome.audioBridgeCommand);
|
|
144
|
+
const agentModeExternalBridgeInvalid = mode === "agent" && hasExternalBridge;
|
|
145
|
+
checks.push({
|
|
146
|
+
id: "audio-bridge",
|
|
147
|
+
ok: mode === "agent" ? hasCommandPair && !agentModeExternalBridgeInvalid : hasExternalBridge || hasCommandPair,
|
|
148
|
+
message: agentModeExternalBridgeInvalid ? "Chrome agent mode requires chrome.audioInputCommand and chrome.audioOutputCommand; chrome.audioBridgeCommand is bidi-only" : hasExternalBridge ? "Chrome audio bridge command configured" : hasCommandPair ? `Chrome command-pair talk-back audio bridge configured (${config.chrome.audioFormat})` : "Chrome talk-back audio bridge not configured"
|
|
149
|
+
});
|
|
150
|
+
} else if (transport === "chrome" || transport === "chrome-node") checks.push({
|
|
151
|
+
id: "audio-bridge",
|
|
152
|
+
ok: true,
|
|
153
|
+
message: "Chrome observe-only mode does not require a realtime audio bridge"
|
|
154
|
+
});
|
|
155
|
+
checks.push({
|
|
156
|
+
id: "guest-join-defaults",
|
|
157
|
+
ok: Boolean(config.chrome.guestName && config.chrome.autoJoin && config.chrome.reuseExistingTab),
|
|
158
|
+
message: config.chrome.guestName && config.chrome.autoJoin && config.chrome.reuseExistingTab ? "Guest auto-join and tab reuse defaults are enabled" : "Set chrome.guestName, chrome.autoJoin, and chrome.reuseExistingTab for unattended guest joins"
|
|
159
|
+
});
|
|
160
|
+
checks.push({
|
|
161
|
+
id: "chrome-node-target",
|
|
162
|
+
ok: config.defaultTransport !== "chrome-node" || Boolean(config.chromeNode.node),
|
|
163
|
+
message: config.defaultTransport === "chrome-node" && !config.chromeNode.node ? "chrome-node default should pin chromeNode.node when multiple nodes may be connected" : config.chromeNode.node ? `Chrome node pinned to ${config.chromeNode.node}` : "Chrome node not pinned; automatic selection works when exactly one capable node is connected"
|
|
164
|
+
});
|
|
165
|
+
if (needsChromeRealtimeAudio) checks.push({
|
|
166
|
+
id: "intro-after-in-call",
|
|
167
|
+
ok: config.chrome.waitForInCallMs > 0,
|
|
168
|
+
message: config.chrome.waitForInCallMs > 0 ? `Realtime intro waits up to ${config.chrome.waitForInCallMs}ms for the Meet tab to be in-call` : "Set chrome.waitForInCallMs to delay realtime intro until the Meet tab is in-call"
|
|
169
|
+
});
|
|
170
|
+
if (transport === "twilio") {
|
|
171
|
+
const hasRequestDialPlan = Boolean(options?.twilioDialInNumber);
|
|
172
|
+
const hasDefaultDialPlan = Boolean(config.twilio.defaultDialInNumber);
|
|
173
|
+
const hasDialPlan = hasRequestDialPlan || hasDefaultDialPlan;
|
|
174
|
+
checks.push({
|
|
175
|
+
id: "twilio-dial-plan",
|
|
176
|
+
ok: hasDialPlan,
|
|
177
|
+
message: hasRequestDialPlan ? "Twilio request includes a Meet dial-in number" : hasDefaultDialPlan ? "Twilio default Meet dial-in number is configured" : "Twilio joins require a Meet dial-in phone number; pass dialInNumber with optional pin/dtmfSequence or configure twilio.defaultDialInNumber"
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (config.voiceCall.enabled && (transport === "twilio" || Boolean(config.twilio.defaultDialInNumber) || Object.hasOwn(pluginEntries, "voice-call"))) {
|
|
181
|
+
const voiceCallAllowed = !Array.isArray(pluginAllow) || pluginAllow.includes("voice-call");
|
|
182
|
+
const voiceCallEnabled = Object.hasOwn(pluginEntries, "voice-call") && voiceCallEntry.enabled !== false;
|
|
183
|
+
checks.push({
|
|
184
|
+
id: "twilio-voice-call-plugin",
|
|
185
|
+
ok: voiceCallAllowed && voiceCallEnabled,
|
|
186
|
+
message: voiceCallAllowed && voiceCallEnabled ? "Twilio transport can delegate dialing to the voice-call plugin" : "Enable plugins.entries.voice-call and include voice-call in plugins.allow for Twilio dialing"
|
|
187
|
+
});
|
|
188
|
+
if ((normalizeOptionalString(voiceCallConfig.provider) ?? "twilio") === "twilio") {
|
|
189
|
+
const accountSid = resolveVoiceCallSetupValue(voiceCallTwilioConfig.accountSid, env.TWILIO_ACCOUNT_SID);
|
|
190
|
+
const authToken = resolveVoiceCallSetupValue(voiceCallTwilioConfig.authToken, env.TWILIO_AUTH_TOKEN);
|
|
191
|
+
const fromNumber = resolveVoiceCallSetupValue(voiceCallConfig.fromNumber, env.TWILIO_FROM_NUMBER);
|
|
192
|
+
const twilioReady = Boolean(accountSid && authToken && fromNumber);
|
|
193
|
+
checks.push({
|
|
194
|
+
id: "twilio-voice-call-credentials",
|
|
195
|
+
ok: twilioReady,
|
|
196
|
+
message: twilioReady ? "Twilio voice-call credentials are configured" : "Set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_FROM_NUMBER or configure voice-call Twilio credentials"
|
|
197
|
+
});
|
|
198
|
+
checks.push(getVoiceCallWebhookExposureCheck(voiceCallConfig));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return createMeetingSetupStatus(checks);
|
|
202
|
+
}
|
|
203
|
+
function addGoogleMeetSetupCheck(status, check) {
|
|
204
|
+
return addMeetingSetupCheck(status, check);
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region extensions/google-meet/src/transports/chrome.ts
|
|
208
|
+
function shouldCaptureCaptions(mode, fullConfig) {
|
|
209
|
+
return mode === "transcribe" || !fullConfig || resolveTranscriptsConfig(fullConfig.transcripts).enabled;
|
|
210
|
+
}
|
|
211
|
+
async function prepareGoogleMeetAudioRuntime(params) {
|
|
212
|
+
const audio = MeetingPlatformAdapter.resolveAudioRuntimeForFormat({
|
|
213
|
+
backend: params.config.chrome.audioBackend,
|
|
214
|
+
bufferBytes: params.config.chrome.audioBufferBytes,
|
|
215
|
+
format: params.config.chrome.audioFormat,
|
|
216
|
+
inputCommand: params.config.chrome.audioInputCommandOverride,
|
|
217
|
+
outputCommand: params.config.chrome.audioOutputCommandOverride
|
|
218
|
+
});
|
|
219
|
+
await MeetingPlatformAdapter.ensureAudioBackend({
|
|
220
|
+
backend: audio.backend,
|
|
221
|
+
timeoutMs: params.timeoutMs,
|
|
222
|
+
run: async (argv, timeoutMs) => {
|
|
223
|
+
const result = await params.runtime.system.runCommandWithTimeout(argv, { timeoutMs });
|
|
224
|
+
return {
|
|
225
|
+
...result,
|
|
226
|
+
code: result.code ?? 1
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
return audio;
|
|
231
|
+
}
|
|
232
|
+
async function assertGoogleMeetAudioAvailable(params) {
|
|
233
|
+
await prepareGoogleMeetAudioRuntime(params);
|
|
234
|
+
}
|
|
235
|
+
async function launchChromeMeet(params) {
|
|
236
|
+
let audio;
|
|
237
|
+
const checkRealtimeAudioPrerequisites = async () => {
|
|
238
|
+
if (!MeetingPlatformAdapter.isTalkBackMode(params.mode)) return;
|
|
239
|
+
audio = await prepareGoogleMeetAudioRuntime({
|
|
240
|
+
runtime: params.runtime,
|
|
241
|
+
config: params.config,
|
|
242
|
+
timeoutMs: Math.min(params.config.chrome.joinTimeoutMs, 1e4)
|
|
243
|
+
});
|
|
244
|
+
if (params.config.chrome.audioBridgeHealthCommand) {
|
|
245
|
+
const health = await params.runtime.system.runCommandWithTimeout(params.config.chrome.audioBridgeHealthCommand, { timeoutMs: params.config.chrome.joinTimeoutMs });
|
|
246
|
+
if (health.code !== 0) throw new Error(`Chrome audio bridge health check failed: ${health.stderr || health.stdout || health.code}`);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const startRealtimeAudioBridge = async () => {
|
|
250
|
+
if (!MeetingPlatformAdapter.isTalkBackMode(params.mode)) return;
|
|
251
|
+
if (params.config.chrome.audioBridgeCommand) {
|
|
252
|
+
if (params.mode === "agent") throw new Error("Chrome agent mode requires chrome.audioInputCommand and chrome.audioOutputCommand so OpenClaw can run STT and regular TTS directly.");
|
|
253
|
+
const bridge = await params.runtime.system.runCommandWithTimeout(params.config.chrome.audioBridgeCommand, { timeoutMs: params.config.chrome.joinTimeoutMs });
|
|
254
|
+
if (bridge.code !== 0) throw new Error(`failed to start Chrome audio bridge: ${bridge.stderr || bridge.stdout || bridge.code}`);
|
|
255
|
+
return { type: "external-command" };
|
|
256
|
+
}
|
|
257
|
+
if (!params.config.chrome.audioInputCommand || !params.config.chrome.audioOutputCommand) throw new Error("Chrome talk-back mode requires chrome.audioInputCommand and chrome.audioOutputCommand, or chrome.audioBridgeCommand for an external bridge.");
|
|
258
|
+
if (!audio) throw new Error("Google Meet audio backend was not prepared.");
|
|
259
|
+
const transport = createLocalMeetingRealtimeAudioTransport({
|
|
260
|
+
inputCommand: audio.inputCommand,
|
|
261
|
+
outputCommand: audio.outputCommand,
|
|
262
|
+
audioFormat: params.config.chrome.audioFormat,
|
|
263
|
+
bargeInInputCommand: params.config.chrome.bargeInInputCommand,
|
|
264
|
+
bargeInRmsThreshold: params.config.chrome.bargeInRmsThreshold,
|
|
265
|
+
bargeInPeakThreshold: params.config.chrome.bargeInPeakThreshold,
|
|
266
|
+
bargeInCooldownMs: params.config.chrome.bargeInCooldownMs,
|
|
267
|
+
logger: params.logger,
|
|
268
|
+
logScope: GOOGLE_MEET_PLATFORM_ADAPTER.logScope
|
|
269
|
+
});
|
|
270
|
+
const bindings = createMeetingRealtimeEngineBindings({
|
|
271
|
+
platform: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
272
|
+
...params
|
|
273
|
+
});
|
|
274
|
+
const engine = params.mode === "agent" ? await startMeetingAgentRealtimeEngine({
|
|
275
|
+
config: params.config,
|
|
276
|
+
fullConfig: params.fullConfig,
|
|
277
|
+
runtime: params.runtime,
|
|
278
|
+
platform: bindings.platform,
|
|
279
|
+
meetingSessionId: params.meetingSessionId,
|
|
280
|
+
requesterSessionKey: params.requesterSessionKey,
|
|
281
|
+
transport,
|
|
282
|
+
logger: params.logger,
|
|
283
|
+
consultAgent: bindings.consultAgent
|
|
284
|
+
}) : await startMeetingRealtimeEngine({
|
|
285
|
+
config: {
|
|
286
|
+
...params.config,
|
|
287
|
+
realtime: {
|
|
288
|
+
...params.config.realtime,
|
|
289
|
+
strategy: "bidi"
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
fullConfig: params.fullConfig,
|
|
293
|
+
runtime: params.runtime,
|
|
294
|
+
...bindings,
|
|
295
|
+
meetingSessionId: params.meetingSessionId,
|
|
296
|
+
requesterSessionKey: params.requesterSessionKey,
|
|
297
|
+
transport,
|
|
298
|
+
logger: params.logger
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
type: "command-pair",
|
|
302
|
+
inputCommand: audio.inputCommand,
|
|
303
|
+
outputCommand: audio.outputCommand,
|
|
304
|
+
...engine
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
await checkRealtimeAudioPrerequisites();
|
|
308
|
+
if (!params.config.chrome.launch) {
|
|
309
|
+
const recovered = await recoverMeetingBrowserTab({
|
|
310
|
+
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
311
|
+
allowSessionAdoption: true,
|
|
312
|
+
autoJoin: params.config.chrome.autoJoin,
|
|
313
|
+
callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
|
|
314
|
+
captureCaptions: shouldCaptureCaptions(params.mode, params.fullConfig),
|
|
315
|
+
config: params.config.chrome,
|
|
316
|
+
locationLabel: "in local Chrome",
|
|
317
|
+
meetingSessionId: params.meetingSessionId,
|
|
318
|
+
mode: params.mode,
|
|
319
|
+
requestedMeetingUrl: params.url,
|
|
320
|
+
trackedMeetingUrl: params.url,
|
|
321
|
+
trackedTargetId: void 0
|
|
322
|
+
});
|
|
323
|
+
const audioBridge = MeetingPlatformAdapter.isRealtimeRouteReady(params.mode, recovered.browser) ? await startRealtimeAudioBridge() : void 0;
|
|
324
|
+
return {
|
|
325
|
+
launched: false,
|
|
326
|
+
audioBackend: audio?.backend,
|
|
327
|
+
audioBridge,
|
|
328
|
+
browser: recovered.browser,
|
|
329
|
+
tab: recovered.targetId ? {
|
|
330
|
+
targetId: recovered.targetId,
|
|
331
|
+
openedByPlugin: false
|
|
332
|
+
} : void 0
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const result = await openMeetingWithBrowser({
|
|
336
|
+
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
337
|
+
callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
|
|
338
|
+
config: params.config.chrome,
|
|
339
|
+
session: {
|
|
340
|
+
captureCaptions: shouldCaptureCaptions(params.mode, params.fullConfig),
|
|
341
|
+
meetingSessionId: params.meetingSessionId,
|
|
342
|
+
mode: params.mode,
|
|
343
|
+
url: params.url
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
const audioBridge = MeetingPlatformAdapter.isRealtimeRouteReady(params.mode, result.browser) ? await startRealtimeAudioBridge() : void 0;
|
|
347
|
+
return {
|
|
348
|
+
...result,
|
|
349
|
+
audioBackend: audio?.backend,
|
|
350
|
+
audioBridge
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function parseNodeStartResult(raw) {
|
|
354
|
+
const value = raw && typeof raw === "object" && "payload" in raw ? raw.payload : raw;
|
|
355
|
+
if (!value || typeof value !== "object") throw new Error("Google Meet node returned an invalid start result.");
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
async function leaveChromeMeet(params) {
|
|
359
|
+
return await leaveMeetingWithBrowser({
|
|
360
|
+
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
361
|
+
callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
|
|
362
|
+
launch: params.config.chrome.launch,
|
|
363
|
+
meetingSessionId: params.meetingSessionId,
|
|
364
|
+
meetingUrl: params.meetingUrl,
|
|
365
|
+
tab: params.tab,
|
|
366
|
+
timeoutMs: params.config.chrome.joinTimeoutMs
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
async function readChromeMeetTranscript(params) {
|
|
370
|
+
return await readMeetingTranscriptWithBrowser({
|
|
371
|
+
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
372
|
+
callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
|
|
373
|
+
finalize: params.finalize === true,
|
|
374
|
+
meetingUrl: params.meetingUrl,
|
|
375
|
+
meetingSessionId: params.meetingSessionId,
|
|
376
|
+
tab: params.tab,
|
|
377
|
+
timeoutMs: Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 1e4)
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
async function readChromeMeetTranscriptOnNode(params) {
|
|
381
|
+
const nodeId = params.nodeId ?? await resolveChromeNode({
|
|
382
|
+
runtime: params.runtime,
|
|
383
|
+
requestedNode: params.config.chromeNode.node
|
|
384
|
+
});
|
|
385
|
+
const timeoutMs = Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 1e4);
|
|
386
|
+
return await readMeetingTranscriptWithBrowser({
|
|
387
|
+
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
388
|
+
callBrowser: async (request) => await callBrowserProxyOnNode({
|
|
389
|
+
runtime: params.runtime,
|
|
390
|
+
nodeId,
|
|
391
|
+
method: request.method,
|
|
392
|
+
path: request.path,
|
|
393
|
+
body: request.body,
|
|
394
|
+
timeoutMs: request.timeoutMs
|
|
395
|
+
}),
|
|
396
|
+
finalize: params.finalize === true,
|
|
397
|
+
meetingUrl: params.meetingUrl,
|
|
398
|
+
meetingSessionId: params.meetingSessionId,
|
|
399
|
+
tab: params.tab,
|
|
400
|
+
timeoutMs
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
async function leaveChromeMeetOnNode(params) {
|
|
404
|
+
const nodeId = params.nodeId ?? await resolveChromeNode({
|
|
405
|
+
runtime: params.runtime,
|
|
406
|
+
requestedNode: params.config.chromeNode.node
|
|
407
|
+
});
|
|
408
|
+
return await leaveMeetingWithBrowser({
|
|
409
|
+
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
410
|
+
callBrowser: async (request) => await callBrowserProxyOnNode({
|
|
411
|
+
runtime: params.runtime,
|
|
412
|
+
nodeId,
|
|
413
|
+
method: request.method,
|
|
414
|
+
path: request.path,
|
|
415
|
+
body: request.body,
|
|
416
|
+
timeoutMs: request.timeoutMs
|
|
417
|
+
}),
|
|
418
|
+
launch: params.config.chrome.launch,
|
|
419
|
+
meetingSessionId: params.meetingSessionId,
|
|
420
|
+
meetingUrl: params.meetingUrl,
|
|
421
|
+
tab: params.tab,
|
|
422
|
+
timeoutMs: params.config.chrome.joinTimeoutMs
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
async function openMeetWithBrowserProxy(params) {
|
|
426
|
+
const callBrowser = async (request) => await callBrowserProxyOnNode({
|
|
427
|
+
runtime: params.runtime,
|
|
428
|
+
nodeId: params.nodeId,
|
|
429
|
+
method: request.method,
|
|
430
|
+
path: request.path,
|
|
431
|
+
body: request.body,
|
|
432
|
+
timeoutMs: request.timeoutMs
|
|
433
|
+
});
|
|
434
|
+
if (!params.config.chrome.launch) {
|
|
435
|
+
const recovered = await recoverMeetingBrowserTab({
|
|
436
|
+
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
437
|
+
allowSessionAdoption: true,
|
|
438
|
+
autoJoin: params.config.chrome.autoJoin,
|
|
439
|
+
callBrowser,
|
|
440
|
+
captureCaptions: params.captureCaptions,
|
|
441
|
+
config: params.config.chrome,
|
|
442
|
+
locationLabel: "on the selected Chrome node",
|
|
443
|
+
meetingSessionId: params.meetingSessionId,
|
|
444
|
+
mode: params.mode,
|
|
445
|
+
requestedMeetingUrl: params.url,
|
|
446
|
+
trackedMeetingUrl: params.url,
|
|
447
|
+
trackedTargetId: void 0
|
|
448
|
+
});
|
|
449
|
+
return {
|
|
450
|
+
launched: false,
|
|
451
|
+
browser: recovered.browser,
|
|
452
|
+
tab: recovered.targetId ? {
|
|
453
|
+
targetId: recovered.targetId,
|
|
454
|
+
openedByPlugin: false
|
|
455
|
+
} : void 0
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return await openMeetingWithBrowser({
|
|
459
|
+
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
460
|
+
callBrowser,
|
|
461
|
+
config: params.config.chrome,
|
|
462
|
+
session: {
|
|
463
|
+
captureCaptions: params.captureCaptions,
|
|
464
|
+
mode: params.mode,
|
|
465
|
+
meetingSessionId: params.meetingSessionId,
|
|
466
|
+
url: params.url
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
async function recoverCurrentMeetTab(params) {
|
|
471
|
+
return {
|
|
472
|
+
transport: "chrome",
|
|
473
|
+
...await recoverMeetingBrowserTab({
|
|
474
|
+
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
475
|
+
callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
|
|
476
|
+
captureCaptions: shouldCaptureCaptions(params.mode ?? "bidi", params.fullConfig),
|
|
477
|
+
config: params.config.chrome,
|
|
478
|
+
locationLabel: "in local Chrome",
|
|
479
|
+
mode: params.mode ?? "bidi",
|
|
480
|
+
readOnly: params.readOnly,
|
|
481
|
+
requestedMeetingUrl: params.url,
|
|
482
|
+
trackedMeetingUrl: params.trackedMeetingUrl,
|
|
483
|
+
trackedTargetId: params.trackedTargetId
|
|
484
|
+
})
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
async function recoverCurrentMeetTabOnNode(params) {
|
|
488
|
+
const nodeId = await resolveChromeNode({
|
|
489
|
+
runtime: params.runtime,
|
|
490
|
+
requestedNode: params.config.chromeNode.node
|
|
491
|
+
});
|
|
492
|
+
return {
|
|
493
|
+
transport: "chrome-node",
|
|
494
|
+
nodeId,
|
|
495
|
+
...await recoverMeetingBrowserTab({
|
|
496
|
+
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
497
|
+
callBrowser: async (request) => await callBrowserProxyOnNode({
|
|
498
|
+
runtime: params.runtime,
|
|
499
|
+
nodeId,
|
|
500
|
+
method: request.method,
|
|
501
|
+
path: request.path,
|
|
502
|
+
body: request.body,
|
|
503
|
+
timeoutMs: request.timeoutMs
|
|
504
|
+
}),
|
|
505
|
+
captureCaptions: shouldCaptureCaptions(params.mode ?? "bidi", params.fullConfig),
|
|
506
|
+
config: params.config.chrome,
|
|
507
|
+
locationLabel: "on the selected Chrome node",
|
|
508
|
+
mode: params.mode ?? "bidi",
|
|
509
|
+
readOnly: params.readOnly,
|
|
510
|
+
requestedMeetingUrl: params.url,
|
|
511
|
+
trackedMeetingUrl: params.trackedMeetingUrl,
|
|
512
|
+
trackedTargetId: params.trackedTargetId
|
|
513
|
+
})
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
async function launchChromeMeetOnNode(params) {
|
|
517
|
+
const nodeId = await resolveChromeNode({
|
|
518
|
+
runtime: params.runtime,
|
|
519
|
+
requestedNode: params.config.chromeNode.node
|
|
520
|
+
});
|
|
521
|
+
try {
|
|
522
|
+
await params.runtime.nodes.invoke({
|
|
523
|
+
nodeId,
|
|
524
|
+
command: GOOGLE_MEET_NODE_COMMAND,
|
|
525
|
+
params: {
|
|
526
|
+
action: "stopByUrl",
|
|
527
|
+
url: params.url,
|
|
528
|
+
mode: params.mode
|
|
529
|
+
},
|
|
530
|
+
timeoutMs: 5e3
|
|
531
|
+
});
|
|
532
|
+
} catch (error) {
|
|
533
|
+
params.logger.debug?.(`[google-meet] node bridge cleanup before join ignored: ${error instanceof Error ? error.message : String(error)}`);
|
|
534
|
+
}
|
|
535
|
+
const setup = MeetingPlatformAdapter.isTalkBackMode(params.mode) ? parseNodeStartResult(await params.runtime.nodes.invoke({
|
|
536
|
+
nodeId,
|
|
537
|
+
command: GOOGLE_MEET_NODE_COMMAND,
|
|
538
|
+
params: {
|
|
539
|
+
action: "setup",
|
|
540
|
+
audioBackend: params.config.chrome.audioBackend,
|
|
541
|
+
audioFormat: params.config.chrome.audioFormat,
|
|
542
|
+
audioBufferBytes: params.config.chrome.audioBufferBytes,
|
|
543
|
+
...params.config.chrome.audioInputCommandOverride ? { audioInputCommand: params.config.chrome.audioInputCommandOverride } : {},
|
|
544
|
+
...params.config.chrome.audioOutputCommandOverride ? { audioOutputCommand: params.config.chrome.audioOutputCommandOverride } : {}
|
|
545
|
+
},
|
|
546
|
+
timeoutMs: 12e3
|
|
547
|
+
})) : void 0;
|
|
548
|
+
const browserControl = await openMeetWithBrowserProxy({
|
|
549
|
+
runtime: params.runtime,
|
|
550
|
+
nodeId,
|
|
551
|
+
config: params.config,
|
|
552
|
+
captureCaptions: shouldCaptureCaptions(params.mode, params.fullConfig),
|
|
553
|
+
mode: params.mode,
|
|
554
|
+
meetingSessionId: params.meetingSessionId,
|
|
555
|
+
url: params.url
|
|
556
|
+
});
|
|
557
|
+
if (MeetingPlatformAdapter.isTalkBackMode(params.mode) && !MeetingPlatformAdapter.isRealtimeRouteReady(params.mode, browserControl.browser)) return {
|
|
558
|
+
nodeId,
|
|
559
|
+
launched: browserControl.launched,
|
|
560
|
+
audioBackend: setup?.audioBackend,
|
|
561
|
+
browser: browserControl.browser,
|
|
562
|
+
tab: browserControl.tab
|
|
563
|
+
};
|
|
564
|
+
const result = parseNodeStartResult(await params.runtime.nodes.invoke({
|
|
565
|
+
nodeId,
|
|
566
|
+
command: GOOGLE_MEET_NODE_COMMAND,
|
|
567
|
+
params: {
|
|
568
|
+
action: "start",
|
|
569
|
+
url: params.url,
|
|
570
|
+
mode: params.mode,
|
|
571
|
+
launch: false,
|
|
572
|
+
browserProfile: params.config.chrome.browserProfile,
|
|
573
|
+
joinTimeoutMs: params.config.chrome.joinTimeoutMs,
|
|
574
|
+
audioBackend: params.config.chrome.audioBackend,
|
|
575
|
+
audioFormat: params.config.chrome.audioFormat,
|
|
576
|
+
audioBufferBytes: params.config.chrome.audioBufferBytes,
|
|
577
|
+
...params.config.chrome.audioInputCommandOverride ? { audioInputCommand: params.config.chrome.audioInputCommandOverride } : {},
|
|
578
|
+
...params.config.chrome.audioOutputCommandOverride ? { audioOutputCommand: params.config.chrome.audioOutputCommandOverride } : {},
|
|
579
|
+
audioBridgeCommand: params.config.chrome.audioBridgeCommand,
|
|
580
|
+
audioBridgeHealthCommand: params.config.chrome.audioBridgeHealthCommand
|
|
581
|
+
},
|
|
582
|
+
timeoutMs: addTimerTimeoutGraceMs(params.config.chrome.joinTimeoutMs) ?? 1
|
|
583
|
+
}));
|
|
584
|
+
if (result.audioBridge?.type === "node-command-pair") {
|
|
585
|
+
if (!result.bridgeId) throw new Error("Google Meet node did not return an audio bridge id.");
|
|
586
|
+
const transport = createNodeMeetingRealtimeAudioTransport({
|
|
587
|
+
runtime: params.runtime,
|
|
588
|
+
nodeId,
|
|
589
|
+
bridgeId: result.bridgeId,
|
|
590
|
+
audioFormat: params.config.chrome.audioFormat,
|
|
591
|
+
logger: params.logger,
|
|
592
|
+
commandName: GOOGLE_MEET_NODE_COMMAND,
|
|
593
|
+
logScope: GOOGLE_MEET_PLATFORM_ADAPTER.logScope,
|
|
594
|
+
logPrefix: params.mode === "agent" ? "node agent" : "node"
|
|
595
|
+
});
|
|
596
|
+
Reflect.set(transport, Symbol.for("openclaw.internal.meeting-node-output-generation.v1"), result.audioBridge.outputGeneration === true);
|
|
597
|
+
const bindings = createMeetingRealtimeEngineBindings({
|
|
598
|
+
platform: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
599
|
+
...params
|
|
600
|
+
});
|
|
601
|
+
const engine = params.mode === "agent" ? await startMeetingAgentRealtimeEngine({
|
|
602
|
+
config: params.config,
|
|
603
|
+
fullConfig: params.fullConfig,
|
|
604
|
+
runtime: params.runtime,
|
|
605
|
+
platform: bindings.platform,
|
|
606
|
+
meetingSessionId: params.meetingSessionId,
|
|
607
|
+
requesterSessionKey: params.requesterSessionKey,
|
|
608
|
+
logPrefix: "node",
|
|
609
|
+
transport,
|
|
610
|
+
logger: params.logger,
|
|
611
|
+
consultAgent: bindings.consultAgent
|
|
612
|
+
}) : await startMeetingRealtimeEngine({
|
|
613
|
+
config: {
|
|
614
|
+
...params.config,
|
|
615
|
+
realtime: {
|
|
616
|
+
...params.config.realtime,
|
|
617
|
+
strategy: "bidi"
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
fullConfig: params.fullConfig,
|
|
621
|
+
runtime: params.runtime,
|
|
622
|
+
...bindings,
|
|
623
|
+
meetingSessionId: params.meetingSessionId,
|
|
624
|
+
requesterSessionKey: params.requesterSessionKey,
|
|
625
|
+
logPrefix: "node",
|
|
626
|
+
talkSessionId: `google-meet:${params.meetingSessionId}:${result.bridgeId}:node-realtime`,
|
|
627
|
+
talkContext: {
|
|
628
|
+
nodeId,
|
|
629
|
+
bridgeId: result.bridgeId
|
|
630
|
+
},
|
|
631
|
+
transport,
|
|
632
|
+
logger: params.logger
|
|
633
|
+
});
|
|
634
|
+
const bridge = {
|
|
635
|
+
type: "node-command-pair",
|
|
636
|
+
nodeId,
|
|
637
|
+
bridgeId: result.bridgeId,
|
|
638
|
+
...engine
|
|
639
|
+
};
|
|
640
|
+
return {
|
|
641
|
+
nodeId,
|
|
642
|
+
launched: browserControl.launched || result.launched === true,
|
|
643
|
+
audioBackend: result.audioBackend ?? setup?.audioBackend,
|
|
644
|
+
audioBridge: bridge,
|
|
645
|
+
browser: browserControl.browser ?? result.browser,
|
|
646
|
+
tab: browserControl.tab
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (result.audioBridge?.type === "external-command") return {
|
|
650
|
+
nodeId,
|
|
651
|
+
launched: browserControl.launched || result.launched === true,
|
|
652
|
+
audioBackend: result.audioBackend ?? setup?.audioBackend,
|
|
653
|
+
audioBridge: { type: "external-command" },
|
|
654
|
+
browser: browserControl.browser ?? result.browser,
|
|
655
|
+
tab: browserControl.tab
|
|
656
|
+
};
|
|
657
|
+
return {
|
|
658
|
+
nodeId,
|
|
659
|
+
launched: browserControl.launched || result.launched === true,
|
|
660
|
+
audioBackend: result.audioBackend ?? setup?.audioBackend,
|
|
661
|
+
browser: browserControl.browser ?? result.browser,
|
|
662
|
+
tab: browserControl.tab
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region extensions/google-meet/src/runtime-setup.ts
|
|
667
|
+
function collectChromeAudioCommands(config) {
|
|
668
|
+
return uniqueStrings((config.chrome.audioBridgeCommand ? [config.chrome.audioBridgeCommand[0]] : [
|
|
669
|
+
config.chrome.audioInputCommand?.[0],
|
|
670
|
+
config.chrome.audioOutputCommand?.[0],
|
|
671
|
+
config.chrome.bargeInInputCommand?.[0]
|
|
672
|
+
]).filter((value) => Boolean(value?.trim())));
|
|
673
|
+
}
|
|
674
|
+
async function commandExists(runtime, command) {
|
|
675
|
+
return (await runtime.system.runCommandWithTimeout([
|
|
676
|
+
"/bin/sh",
|
|
677
|
+
"-lc",
|
|
678
|
+
"command -v \"$1\" >/dev/null 2>&1",
|
|
679
|
+
"sh",
|
|
680
|
+
command
|
|
681
|
+
], { timeoutMs: 5e3 })).code === 0;
|
|
682
|
+
}
|
|
683
|
+
async function getGoogleMeetRuntimeSetupStatus(params) {
|
|
684
|
+
const options = params.options ?? {};
|
|
685
|
+
const transport = options.transport ?? params.config.defaultTransport;
|
|
686
|
+
const mode = options.mode === "realtime" ? "agent" : options.mode ?? params.config.defaultMode;
|
|
687
|
+
const twilioDialInNumber = transport === "twilio" ? normalizeDialInNumber(options.dialInNumber) : void 0;
|
|
688
|
+
const shouldCheckChromeNode = transport === "chrome-node" || !options.transport && Boolean(params.config.chromeNode.node);
|
|
689
|
+
let status = getGoogleMeetSetupStatus(params.config, {
|
|
690
|
+
fullConfig: params.fullConfig,
|
|
691
|
+
mode,
|
|
692
|
+
transport,
|
|
693
|
+
twilioDialInNumber
|
|
694
|
+
});
|
|
695
|
+
if (shouldCheckChromeNode) try {
|
|
696
|
+
const node = await resolveChromeNodeInfo({
|
|
697
|
+
runtime: params.runtime,
|
|
698
|
+
requestedNode: params.config.chromeNode.node
|
|
699
|
+
});
|
|
700
|
+
const label = node.displayName ?? node.remoteIp ?? node.nodeId ?? "connected node";
|
|
701
|
+
status = addGoogleMeetSetupCheck(status, {
|
|
702
|
+
id: "chrome-node-connected",
|
|
703
|
+
ok: true,
|
|
704
|
+
message: `Connected Google Meet node ready: ${label}`
|
|
705
|
+
});
|
|
706
|
+
if ((mode === "agent" || mode === "bidi") && node.nodeId) {
|
|
707
|
+
const setup = await params.runtime.nodes.invoke({
|
|
708
|
+
nodeId: node.nodeId,
|
|
709
|
+
command: GOOGLE_MEET_NODE_COMMAND,
|
|
710
|
+
params: {
|
|
711
|
+
action: "setup",
|
|
712
|
+
audioBackend: params.config.chrome.audioBackend,
|
|
713
|
+
audioFormat: params.config.chrome.audioFormat,
|
|
714
|
+
audioBufferBytes: params.config.chrome.audioBufferBytes,
|
|
715
|
+
...params.config.chrome.audioInputCommandOverride ? { audioInputCommand: params.config.chrome.audioInputCommandOverride } : {},
|
|
716
|
+
...params.config.chrome.audioOutputCommandOverride ? { audioOutputCommand: params.config.chrome.audioOutputCommandOverride } : {}
|
|
717
|
+
},
|
|
718
|
+
timeoutMs: 12e3
|
|
719
|
+
});
|
|
720
|
+
status = addGoogleMeetSetupCheck(status, {
|
|
721
|
+
id: "chrome-node-audio-prerequisites",
|
|
722
|
+
ok: true,
|
|
723
|
+
message: setup ? "Remote virtual audio backend and command-pair prerequisites are ready" : "Remote audio setup completed"
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
} catch (error) {
|
|
727
|
+
status = addGoogleMeetSetupCheck(status, {
|
|
728
|
+
id: "chrome-node-connected",
|
|
729
|
+
ok: false,
|
|
730
|
+
message: formatErrorMessage(error)
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
if (transport !== "chrome" || mode !== "agent" && mode !== "bidi") return status;
|
|
734
|
+
try {
|
|
735
|
+
await assertGoogleMeetAudioAvailable({
|
|
736
|
+
runtime: params.runtime,
|
|
737
|
+
config: params.config,
|
|
738
|
+
timeoutMs: Math.min(params.config.chrome.joinTimeoutMs, 1e4)
|
|
739
|
+
});
|
|
740
|
+
status = addGoogleMeetSetupCheck(status, {
|
|
741
|
+
id: "chrome-local-audio-device",
|
|
742
|
+
ok: true,
|
|
743
|
+
message: "Virtual meeting audio backend is ready"
|
|
744
|
+
});
|
|
745
|
+
} catch (error) {
|
|
746
|
+
status = addGoogleMeetSetupCheck(status, {
|
|
747
|
+
id: "chrome-local-audio-device",
|
|
748
|
+
ok: false,
|
|
749
|
+
message: formatErrorMessage(error)
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
const commands = collectChromeAudioCommands(params.config);
|
|
753
|
+
const missingCommands = [];
|
|
754
|
+
for (const command of commands) try {
|
|
755
|
+
if (!await commandExists(params.runtime, command)) missingCommands.push(command);
|
|
756
|
+
} catch {
|
|
757
|
+
missingCommands.push(command);
|
|
758
|
+
}
|
|
759
|
+
return addGoogleMeetSetupCheck(status, {
|
|
760
|
+
id: "chrome-local-audio-commands",
|
|
761
|
+
ok: commands.length > 0 && missingCommands.length === 0,
|
|
762
|
+
message: commands.length === 0 ? "Chrome talk-back audio commands are not configured" : missingCommands.length === 0 ? `Chrome audio command${commands.length === 1 ? "" : "s"} available: ${commands.join(", ")}` : `Chrome audio command${missingCommands.length === 1 ? "" : "s"} missing: ${missingCommands.join(", ")}`
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
//#endregion
|
|
766
|
+
//#region extensions/google-meet/src/voice-call-gateway.ts
|
|
767
|
+
const GOOGLE_MEET_VOICE_CALL_SURFACE = {
|
|
768
|
+
clientDisplayName: "Google Meet plugin",
|
|
769
|
+
configPath: "google-meet voiceCall.gatewayUrl",
|
|
770
|
+
logScope: "[google-meet]",
|
|
771
|
+
meetingLabel: "Meet",
|
|
772
|
+
providerLabel: "Twilio"
|
|
773
|
+
};
|
|
774
|
+
async function createConnectedGatewayClient(params) {
|
|
775
|
+
let client;
|
|
776
|
+
const abortStart = new AbortController();
|
|
777
|
+
let timer;
|
|
778
|
+
try {
|
|
779
|
+
await new Promise((resolve, reject) => {
|
|
780
|
+
timer = setTimeout(() => {
|
|
781
|
+
abortStart.abort();
|
|
782
|
+
reject(/* @__PURE__ */ new Error("gateway connect timeout"));
|
|
783
|
+
}, params.config.requestTimeoutMs);
|
|
784
|
+
client = new GatewayClient({
|
|
785
|
+
url: params.config.gatewayUrl,
|
|
786
|
+
token: params.config.token,
|
|
787
|
+
requestTimeoutMs: params.config.requestTimeoutMs,
|
|
788
|
+
clientName: "cli",
|
|
789
|
+
clientDisplayName: params.surface.clientDisplayName,
|
|
790
|
+
scopes: ["operator.write"],
|
|
791
|
+
onHelloOk: () => {
|
|
792
|
+
clearTimeout(timer);
|
|
793
|
+
resolve();
|
|
794
|
+
},
|
|
795
|
+
onConnectError: (error) => {
|
|
796
|
+
clearTimeout(timer);
|
|
797
|
+
abortStart.abort();
|
|
798
|
+
reject(error);
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
startGatewayClientWhenEventLoopReady(client, {
|
|
802
|
+
timeoutMs: params.config.requestTimeoutMs,
|
|
803
|
+
signal: abortStart.signal
|
|
804
|
+
}).then((readiness) => {
|
|
805
|
+
if (!readiness.ready && !readiness.aborted) {
|
|
806
|
+
clearTimeout(timer);
|
|
807
|
+
reject(/* @__PURE__ */ new Error("gateway event loop readiness timeout"));
|
|
808
|
+
}
|
|
809
|
+
}).catch((error) => {
|
|
810
|
+
clearTimeout(timer);
|
|
811
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
return client;
|
|
815
|
+
} catch (error) {
|
|
816
|
+
clearTimeout(timer);
|
|
817
|
+
abortStart.abort();
|
|
818
|
+
await client?.stopAndWait().catch(() => {});
|
|
819
|
+
throw error;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function createVoiceCallGateway(params) {
|
|
823
|
+
return createMeetingVoiceCallGateway({
|
|
824
|
+
config: params.config.voiceCall,
|
|
825
|
+
runtime: params.runtime,
|
|
826
|
+
surface: GOOGLE_MEET_VOICE_CALL_SURFACE,
|
|
827
|
+
connectClient: createConnectedGatewayClient
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
const isVoiceCallMissingError = isMeetingVoiceCallMissingError;
|
|
831
|
+
async function joinMeetViaVoiceCallGateway(params) {
|
|
832
|
+
return await joinMeetingViaVoiceCallGateway({
|
|
833
|
+
...params,
|
|
834
|
+
config: params.config.voiceCall,
|
|
835
|
+
surface: GOOGLE_MEET_VOICE_CALL_SURFACE
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
async function endMeetVoiceCallGatewayCall(params) {
|
|
839
|
+
await endMeetingVoiceCallGatewayCall(params);
|
|
840
|
+
}
|
|
841
|
+
async function getMeetVoiceCallGatewayCall(params) {
|
|
842
|
+
return await getMeetingVoiceCallGatewayCall(params);
|
|
843
|
+
}
|
|
844
|
+
async function speakMeetViaVoiceCallGateway(params) {
|
|
845
|
+
await speakMeetingViaVoiceCallGateway(params);
|
|
846
|
+
}
|
|
847
|
+
//#endregion
|
|
848
|
+
//#region extensions/google-meet/src/runtime.ts
|
|
849
|
+
const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
850
|
+
var GoogleMeetRuntime = class {
|
|
851
|
+
#createdBrowserTabs;
|
|
852
|
+
#voiceCallGateway;
|
|
853
|
+
#sessions;
|
|
854
|
+
constructor(params) {
|
|
855
|
+
this.params = params;
|
|
856
|
+
this.#createdBrowserTabs = /* @__PURE__ */ new Map();
|
|
857
|
+
this.transcriptSourceRuntime = () => this.#sessions;
|
|
858
|
+
const adapter = GOOGLE_MEET_PLATFORM_ADAPTER;
|
|
859
|
+
this.#voiceCallGateway = createVoiceCallGateway(params);
|
|
860
|
+
this.#sessions = new MeetingSessionRuntime({
|
|
861
|
+
logger: params.logger,
|
|
862
|
+
logScope: "[google-meet]",
|
|
863
|
+
formatError: formatErrorMessage,
|
|
864
|
+
reuseExistingBrowserTab: params.config.chrome.reuseExistingTab,
|
|
865
|
+
waitForInCallMs: params.config.chrome.waitForInCallMs,
|
|
866
|
+
joinTimeoutMs: params.config.chrome.joinTimeoutMs,
|
|
867
|
+
defaultSpeechInstructions: params.config.realtime.introMessage,
|
|
868
|
+
transientSpeechBlockedReasons: /* @__PURE__ */ new Set([
|
|
869
|
+
"not-in-call",
|
|
870
|
+
"browser-unverified",
|
|
871
|
+
"meet-microphone-muted"
|
|
872
|
+
]),
|
|
873
|
+
messages: {
|
|
874
|
+
previousBrowserLeaveFailed: "Could not leave the previous Meet browser tab before reassignment.",
|
|
875
|
+
reassignedSessionNote: "Ended before the same Meet tab was reassigned to another agent.",
|
|
876
|
+
reusedSessionNote: "Reused existing active Meet session.",
|
|
877
|
+
replacementBrowserLeaveFailed: "Could not leave the previous Meet browser tab before reassignment.",
|
|
878
|
+
speechBlockedFallback: "Realtime speech blocked until Google Meet is ready.",
|
|
879
|
+
speech: {
|
|
880
|
+
audioBridgeUnavailable: "Realtime speech requires an active Chrome audio bridge.",
|
|
881
|
+
browserUnverified: "Google Meet browser state has not been verified yet.",
|
|
882
|
+
microphoneMuted: "Turn on the OpenClaw Google Meet microphone before asking OpenClaw to speak.",
|
|
883
|
+
microphoneMutedReason: "meet-microphone-muted",
|
|
884
|
+
notInCall: "Google Meet has not reported that the browser participant is in the call.",
|
|
885
|
+
notInCallReason: "not-in-call",
|
|
886
|
+
browserUnverifiedReason: "browser-unverified",
|
|
887
|
+
audioBridgeUnavailableReason: "audio-bridge-unavailable"
|
|
888
|
+
}
|
|
889
|
+
},
|
|
890
|
+
resolveJoin: (request) => ({
|
|
891
|
+
url: adapter.urls.validateAndNormalize(request.url),
|
|
892
|
+
transport: resolveTransport(request.transport, params.config),
|
|
893
|
+
mode: resolveMode(request.mode, params.config),
|
|
894
|
+
agentId: this.#resolveAgentId(request.agentId)
|
|
895
|
+
}),
|
|
896
|
+
createSession: ({ resolved, createdAt }) => createMeetingSession({
|
|
897
|
+
platform: adapter,
|
|
898
|
+
config: params.config,
|
|
899
|
+
resolved,
|
|
900
|
+
createdAt
|
|
901
|
+
}),
|
|
902
|
+
resolveSpeechInstructions: (request) => request.message ?? params.config.realtime.introMessage,
|
|
903
|
+
isBrowserTransport,
|
|
904
|
+
isTalkBackMode: (mode) => MeetingPlatformAdapter.isTalkBackMode(mode),
|
|
905
|
+
isTranscribeMode: (mode) => mode === "transcribe",
|
|
906
|
+
sameMeetingUrl: (left, right) => adapter.urls.isSameMeeting(left, right),
|
|
907
|
+
normalizeMeetingUrlForReuse: (url) => adapter.urls.normalizeForReuse(url),
|
|
908
|
+
getBrowser: (session) => session.chrome ? {
|
|
909
|
+
launched: session.chrome.launched,
|
|
910
|
+
nodeId: session.chrome.nodeId,
|
|
911
|
+
tab: session.chrome.browserTab,
|
|
912
|
+
health: session.chrome.health,
|
|
913
|
+
hasAudioBridge: Boolean(session.chrome.audioBridge)
|
|
914
|
+
} : void 0,
|
|
915
|
+
setBrowserTab: (session, tab) => {
|
|
916
|
+
if (session.chrome) session.chrome.browserTab = tab;
|
|
917
|
+
},
|
|
918
|
+
setBrowserHealth: (session, health) => {
|
|
919
|
+
if (session.chrome) session.chrome.health = health;
|
|
920
|
+
},
|
|
921
|
+
joinTransport: async ({ request, session, context }) => await this.#joinTransport(request, session, context),
|
|
922
|
+
releaseBrowserTab: async (session) => await this.#releaseBrowserTab(session),
|
|
923
|
+
refreshBrowserHealth: async (session, options) => await this.#refreshBrowserHealth(session, options),
|
|
924
|
+
refreshStatus: async (session) => await this.#refreshStatus(session),
|
|
925
|
+
refreshReusableSession: async (session, _request, _resolved) => {
|
|
926
|
+
if (session.transport === "twilio") await this.#refreshTwilioVoiceCallStatus(session);
|
|
927
|
+
},
|
|
928
|
+
ensureRealtimeBridge: async (session) => await this.#ensureChromeRealtimeBridge(session),
|
|
929
|
+
captureTranscript: async (session, options) => await this.#captureTranscript(session, options),
|
|
930
|
+
speakViaTransport: async (session, instructions) => await this.#speakViaTransport(session, instructions),
|
|
931
|
+
durableTranscripts: {
|
|
932
|
+
config: params.fullConfig.transcripts,
|
|
933
|
+
providerId: "google-meet",
|
|
934
|
+
providerName: "Google Meet"
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
list() {
|
|
939
|
+
return this.#sessions.list();
|
|
940
|
+
}
|
|
941
|
+
async status(sessionId) {
|
|
942
|
+
return await this.#sessions.status(sessionId);
|
|
943
|
+
}
|
|
944
|
+
async transcript(sessionId, options = {}) {
|
|
945
|
+
return await this.#sessions.transcript(sessionId, options);
|
|
946
|
+
}
|
|
947
|
+
async setupStatus(options = {}) {
|
|
948
|
+
return await getGoogleMeetRuntimeSetupStatus({
|
|
949
|
+
config: this.params.config,
|
|
950
|
+
fullConfig: this.params.fullConfig,
|
|
951
|
+
runtime: this.params.runtime,
|
|
952
|
+
options
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
async createViaBrowser() {
|
|
956
|
+
const result = await GOOGLE_MEET_PLATFORM_ADAPTER.create.browser({
|
|
957
|
+
runtime: this.params.runtime,
|
|
958
|
+
config: this.params.config
|
|
959
|
+
});
|
|
960
|
+
if (result.openedByPlugin && result.targetId) this.#createdBrowserTabs.set(`${result.nodeId}:${result.targetId}`, result.meetingUri);
|
|
961
|
+
return result;
|
|
962
|
+
}
|
|
963
|
+
async recoverCurrentTab(request = {}) {
|
|
964
|
+
const transport = resolveTransport(request.transport, this.params.config);
|
|
965
|
+
if (transport === "twilio") throw new Error("recover_current_tab only supports chrome or chrome-node transports");
|
|
966
|
+
const url = request.url ? GOOGLE_MEET_PLATFORM_ADAPTER.urls.validateAndNormalize(request.url) : void 0;
|
|
967
|
+
return transport === "chrome-node" ? await recoverCurrentMeetTabOnNode({
|
|
968
|
+
runtime: this.params.runtime,
|
|
969
|
+
config: this.params.config,
|
|
970
|
+
fullConfig: this.params.fullConfig,
|
|
971
|
+
url
|
|
972
|
+
}) : await recoverCurrentMeetTab({
|
|
973
|
+
runtime: this.params.runtime,
|
|
974
|
+
config: this.params.config,
|
|
975
|
+
fullConfig: this.params.fullConfig,
|
|
976
|
+
url
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
async join(request) {
|
|
980
|
+
return await this.#sessions.join(request);
|
|
981
|
+
}
|
|
982
|
+
async leave(sessionId, options) {
|
|
983
|
+
return await this.#sessions.leave(sessionId, options);
|
|
984
|
+
}
|
|
985
|
+
async speak(sessionId, instructions) {
|
|
986
|
+
return await this.#sessions.speak(sessionId, instructions);
|
|
987
|
+
}
|
|
988
|
+
async testSpeech(request) {
|
|
989
|
+
return await testGoogleMeetSpeech(this.#probeContext(), request);
|
|
990
|
+
}
|
|
991
|
+
async testListen(request) {
|
|
992
|
+
return await testGoogleMeetListening(this.#probeContext(), request);
|
|
993
|
+
}
|
|
994
|
+
#probeContext() {
|
|
995
|
+
return {
|
|
996
|
+
config: this.params.config,
|
|
997
|
+
resolveAgentId: (request) => this.#resolveAgentId(request.agentId),
|
|
998
|
+
list: () => this.list(),
|
|
999
|
+
join: async (request) => await this.join(request),
|
|
1000
|
+
isReusable: (session, resolved) => this.#sessions.isReusableSession(session, resolved),
|
|
1001
|
+
hasHealthHandle: (sessionId) => this.#sessions.hasHealthHandle(sessionId),
|
|
1002
|
+
refreshHealth: (sessionId) => this.#sessions.refreshHealth(sessionId),
|
|
1003
|
+
refreshCaptionHealth: async (session) => await this.#sessions.refreshCaptionHealth(session)
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
#resolveAgentId(requestedAgentId) {
|
|
1007
|
+
return normalizeAgentId(requestedAgentId ?? this.params.config.realtime.agentId ?? resolveDefaultAgentId(this.params.fullConfig));
|
|
1008
|
+
}
|
|
1009
|
+
async #joinTransport(request, session, context) {
|
|
1010
|
+
if (isBrowserTransport(session.transport)) {
|
|
1011
|
+
const chromeConfig = withSessionAgentConfig(this.params.config, session.agentId);
|
|
1012
|
+
const result = session.transport === "chrome-node" ? await launchChromeMeetOnNode({
|
|
1013
|
+
runtime: this.params.runtime,
|
|
1014
|
+
config: chromeConfig,
|
|
1015
|
+
fullConfig: this.params.fullConfig,
|
|
1016
|
+
meetingSessionId: session.id,
|
|
1017
|
+
requesterSessionKey: request.requesterSessionKey,
|
|
1018
|
+
mode: session.mode,
|
|
1019
|
+
url: session.url,
|
|
1020
|
+
logger: this.params.logger
|
|
1021
|
+
}) : await launchChromeMeet({
|
|
1022
|
+
runtime: this.params.runtime,
|
|
1023
|
+
config: chromeConfig,
|
|
1024
|
+
fullConfig: this.params.fullConfig,
|
|
1025
|
+
meetingSessionId: session.id,
|
|
1026
|
+
requesterSessionKey: request.requesterSessionKey,
|
|
1027
|
+
mode: session.mode,
|
|
1028
|
+
url: session.url,
|
|
1029
|
+
logger: this.params.logger
|
|
1030
|
+
});
|
|
1031
|
+
const nodeId = "nodeId" in result ? result.nodeId : void 0;
|
|
1032
|
+
let tab = result.tab;
|
|
1033
|
+
const createdKey = session.transport === "chrome-node" && nodeId && tab ? `${nodeId}:${tab.targetId}` : void 0;
|
|
1034
|
+
const createdUrl = createdKey ? this.#createdBrowserTabs.get(createdKey) : void 0;
|
|
1035
|
+
if (createdKey) this.#createdBrowserTabs.delete(createdKey);
|
|
1036
|
+
if (tab && GOOGLE_MEET_PLATFORM_ADAPTER.urls.isSameMeeting(createdUrl, session.url)) tab = {
|
|
1037
|
+
...tab,
|
|
1038
|
+
openedByPlugin: true
|
|
1039
|
+
};
|
|
1040
|
+
tab = context.inheritedBrowserTab({
|
|
1041
|
+
session,
|
|
1042
|
+
transport: session.transport,
|
|
1043
|
+
nodeId,
|
|
1044
|
+
meetingUrl: session.url,
|
|
1045
|
+
tab
|
|
1046
|
+
});
|
|
1047
|
+
session.chrome = {
|
|
1048
|
+
audioBackend: result.audioBackend,
|
|
1049
|
+
launched: result.launched,
|
|
1050
|
+
nodeId,
|
|
1051
|
+
browserProfile: this.params.config.chrome.browserProfile,
|
|
1052
|
+
browserTab: tab,
|
|
1053
|
+
health: result.browser
|
|
1054
|
+
};
|
|
1055
|
+
const handles = this.#attachChromeAudioBridge(session, result.audioBridge);
|
|
1056
|
+
if (handles) context.attachRuntimeHandles(session, handles);
|
|
1057
|
+
session.notes.push(result.audioBridge ? session.transport === "chrome-node" ? "Chrome node transport joins as the signed-in Google profile on the selected node and routes realtime audio through the node bridge." : "Chrome transport joins as the signed-in Google profile and routes realtime audio through the configured bridge." : MeetingPlatformAdapter.isTalkBackMode(session.mode) ? "Chrome transport is waiting for verified virtual input/output audio routing." : "Chrome transport joins as the signed-in Google profile without starting the realtime audio bridge.");
|
|
1058
|
+
this.#sessions.refreshSpeechReadiness(session);
|
|
1059
|
+
return {};
|
|
1060
|
+
}
|
|
1061
|
+
const dialPlan = GOOGLE_MEET_PLATFORM_ADAPTER.dialIn.buildPlan({
|
|
1062
|
+
dialInNumber: request.dialInNumber,
|
|
1063
|
+
defaultDialInNumber: this.params.config.twilio.defaultDialInNumber,
|
|
1064
|
+
pin: request.pin,
|
|
1065
|
+
defaultPin: this.params.config.twilio.defaultPin,
|
|
1066
|
+
dtmfSequence: request.dtmfSequence,
|
|
1067
|
+
defaultDtmfSequence: this.params.config.twilio.defaultDtmfSequence,
|
|
1068
|
+
dtmfDelayMs: this.params.config.voiceCall.dtmfDelayMs
|
|
1069
|
+
});
|
|
1070
|
+
const dialInNumber = dialPlan.number;
|
|
1071
|
+
if (!dialInNumber) throw new Error("Twilio transport requires a Meet dial-in phone number. Google Meet URLs do not include dial-in details; pass dialInNumber with optional pin/dtmfSequence, configure twilio.defaultDialInNumber, or use chrome/chrome-node transport.");
|
|
1072
|
+
const dtmfSequence = dialPlan.dtmfSequence;
|
|
1073
|
+
const delegatedAgentId = Boolean(normalizeOptionalString(request.agentId) || normalizeOptionalString(this.params.config.realtime.agentId)) ? session.agentId : void 0;
|
|
1074
|
+
const voiceCallResult = this.params.config.voiceCall.enabled ? await joinMeetViaVoiceCallGateway({
|
|
1075
|
+
config: this.params.config,
|
|
1076
|
+
gateway: this.#voiceCallGateway,
|
|
1077
|
+
dialInNumber,
|
|
1078
|
+
dtmfSequence,
|
|
1079
|
+
logger: this.params.logger,
|
|
1080
|
+
...request.requesterSessionKey ? { requesterSessionKey: request.requesterSessionKey } : {},
|
|
1081
|
+
agentId: delegatedAgentId,
|
|
1082
|
+
sessionKey: delegatedAgentId ? `agent:${delegatedAgentId}:google-meet:${session.id}` : `voice:google-meet:${session.id}`,
|
|
1083
|
+
message: MeetingPlatformAdapter.isTalkBackMode(session.mode) ? request.message ?? this.params.config.voiceCall.introMessage ?? this.params.config.realtime.introMessage : void 0
|
|
1084
|
+
}) : void 0;
|
|
1085
|
+
session.twilio = {
|
|
1086
|
+
dialInNumber,
|
|
1087
|
+
pinProvided: Boolean(dialPlan.pin),
|
|
1088
|
+
dtmfSequence,
|
|
1089
|
+
voiceCallId: voiceCallResult?.callId,
|
|
1090
|
+
dtmfSent: voiceCallResult?.dtmfSent,
|
|
1091
|
+
introSent: voiceCallResult?.introSent
|
|
1092
|
+
};
|
|
1093
|
+
if (voiceCallResult?.callId) context.attachRuntimeHandles(session, { stop: async () => {
|
|
1094
|
+
await endMeetVoiceCallGatewayCall({
|
|
1095
|
+
gateway: this.#voiceCallGateway,
|
|
1096
|
+
callId: voiceCallResult.callId
|
|
1097
|
+
});
|
|
1098
|
+
} });
|
|
1099
|
+
session.notes.push(this.params.config.voiceCall.enabled ? dtmfSequence ? "Twilio transport delegated the phone leg to the voice-call plugin, then queued configured DTMF before realtime connect." : "Twilio transport delegated the call to the voice-call plugin without configured DTMF." : "Twilio transport is an explicit dial plan; voice-call delegation is disabled.");
|
|
1100
|
+
return { delegatedSpoken: Boolean(voiceCallResult?.introSent) };
|
|
1101
|
+
}
|
|
1102
|
+
#attachChromeAudioBridge(session, audioBridge) {
|
|
1103
|
+
if (!session.chrome || !audioBridge) return;
|
|
1104
|
+
session.chrome.audioBridge = {
|
|
1105
|
+
type: audioBridge.type,
|
|
1106
|
+
provider: audioBridge.type === "command-pair" || audioBridge.type === "node-command-pair" ? audioBridge.providerId : void 0
|
|
1107
|
+
};
|
|
1108
|
+
return audioBridge.type === "command-pair" || audioBridge.type === "node-command-pair" ? {
|
|
1109
|
+
stop: audioBridge.stop,
|
|
1110
|
+
speak: audioBridge.speak,
|
|
1111
|
+
getHealth: audioBridge.getHealth
|
|
1112
|
+
} : void 0;
|
|
1113
|
+
}
|
|
1114
|
+
async #ensureChromeRealtimeBridge(session) {
|
|
1115
|
+
if (!MeetingPlatformAdapter.isTalkBackMode(session.mode) || !isBrowserTransport(session.transport) || session.state !== "active" || !session.chrome || session.chrome.audioBridge || !MeetingPlatformAdapter.isRealtimeRouteReady(session.mode, session.chrome.health)) return;
|
|
1116
|
+
const config = withSessionAgentConfig(this.params.config, session.agentId);
|
|
1117
|
+
const recoveryConfig = {
|
|
1118
|
+
...config,
|
|
1119
|
+
chrome: {
|
|
1120
|
+
...config.chrome,
|
|
1121
|
+
launch: false
|
|
1122
|
+
},
|
|
1123
|
+
...session.chrome.nodeId ? { chromeNode: {
|
|
1124
|
+
...config.chromeNode,
|
|
1125
|
+
node: session.chrome.nodeId
|
|
1126
|
+
} } : {}
|
|
1127
|
+
};
|
|
1128
|
+
const result = session.transport === "chrome-node" ? await launchChromeMeetOnNode({
|
|
1129
|
+
runtime: this.params.runtime,
|
|
1130
|
+
config: recoveryConfig,
|
|
1131
|
+
fullConfig: this.params.fullConfig,
|
|
1132
|
+
meetingSessionId: session.id,
|
|
1133
|
+
mode: session.mode,
|
|
1134
|
+
url: session.url,
|
|
1135
|
+
logger: this.params.logger
|
|
1136
|
+
}) : await launchChromeMeet({
|
|
1137
|
+
runtime: this.params.runtime,
|
|
1138
|
+
config: recoveryConfig,
|
|
1139
|
+
fullConfig: this.params.fullConfig,
|
|
1140
|
+
meetingSessionId: session.id,
|
|
1141
|
+
mode: session.mode,
|
|
1142
|
+
url: session.url,
|
|
1143
|
+
logger: this.params.logger
|
|
1144
|
+
});
|
|
1145
|
+
session.updatedAt = nowIso();
|
|
1146
|
+
return this.#attachChromeAudioBridge(session, result.audioBridge);
|
|
1147
|
+
}
|
|
1148
|
+
async #refreshBrowserHealth(session, options = {}) {
|
|
1149
|
+
try {
|
|
1150
|
+
const result = session.transport === "chrome-node" ? await recoverCurrentMeetTabOnNode({
|
|
1151
|
+
runtime: this.params.runtime,
|
|
1152
|
+
config: this.params.config,
|
|
1153
|
+
fullConfig: this.params.fullConfig,
|
|
1154
|
+
mode: session.mode,
|
|
1155
|
+
readOnly: options.readOnly,
|
|
1156
|
+
trackedMeetingUrl: session.url,
|
|
1157
|
+
trackedTargetId: session.chrome?.browserTab?.targetId,
|
|
1158
|
+
url: session.url
|
|
1159
|
+
}) : await recoverCurrentMeetTab({
|
|
1160
|
+
runtime: this.params.runtime,
|
|
1161
|
+
config: this.params.config,
|
|
1162
|
+
fullConfig: this.params.fullConfig,
|
|
1163
|
+
mode: session.mode,
|
|
1164
|
+
readOnly: options.readOnly,
|
|
1165
|
+
trackedMeetingUrl: session.url,
|
|
1166
|
+
trackedTargetId: session.chrome?.browserTab?.targetId,
|
|
1167
|
+
url: session.url
|
|
1168
|
+
});
|
|
1169
|
+
if (result.found && session.chrome) {
|
|
1170
|
+
if (result.targetId) {
|
|
1171
|
+
const currentTab = session.chrome.browserTab;
|
|
1172
|
+
session.chrome.browserTab = {
|
|
1173
|
+
targetId: result.targetId,
|
|
1174
|
+
openedByPlugin: result.targetId === currentTab?.targetId ? currentTab.openedByPlugin : false
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
if (result.browser) session.chrome.health = {
|
|
1178
|
+
...session.chrome.health,
|
|
1179
|
+
...result.browser
|
|
1180
|
+
};
|
|
1181
|
+
session.updatedAt = nowIso();
|
|
1182
|
+
}
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
this.params.logger.debug?.(`[google-meet] browser readiness refresh ignored: ${formatErrorMessage(error)}`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async #refreshStatus(session) {
|
|
1188
|
+
if (isBrowserTransport(session.transport)) await this.#sessions.refreshBrowserHealth(session, {
|
|
1189
|
+
force: true,
|
|
1190
|
+
readOnly: true
|
|
1191
|
+
});
|
|
1192
|
+
else if (session.transport === "twilio") await this.#refreshTwilioVoiceCallStatus(session);
|
|
1193
|
+
else this.#sessions.refreshSpeechReadiness(session);
|
|
1194
|
+
}
|
|
1195
|
+
async #refreshTwilioVoiceCallStatus(session) {
|
|
1196
|
+
const callId = session.twilio?.voiceCallId;
|
|
1197
|
+
if (!callId || session.state !== "active") {
|
|
1198
|
+
this.#sessions.refreshSpeechReadiness(session);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
try {
|
|
1202
|
+
const status = await getMeetVoiceCallGatewayCall({
|
|
1203
|
+
gateway: this.#voiceCallGateway,
|
|
1204
|
+
callId
|
|
1205
|
+
});
|
|
1206
|
+
const call = asOptionalRecord(status.call);
|
|
1207
|
+
if (status.found === false || call?.endedAt !== void 0 || call?.endReason !== void 0) this.#sessions.markSessionEnded(session, "Voice Call is no longer active.");
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
this.params.logger.debug?.(`[google-meet] voice-call status refresh ignored: ${formatErrorMessage(error)}`);
|
|
1210
|
+
}
|
|
1211
|
+
this.#sessions.refreshSpeechReadiness(session);
|
|
1212
|
+
}
|
|
1213
|
+
async #speakViaTransport(session, instructions) {
|
|
1214
|
+
if (session.transport !== "twilio" || !session.twilio?.voiceCallId) return;
|
|
1215
|
+
try {
|
|
1216
|
+
await speakMeetViaVoiceCallGateway({
|
|
1217
|
+
gateway: this.#voiceCallGateway,
|
|
1218
|
+
callId: session.twilio.voiceCallId,
|
|
1219
|
+
message: instructions || this.params.config.voiceCall.introMessage || this.params.config.realtime.introMessage || ""
|
|
1220
|
+
});
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
if (!isVoiceCallMissingError(error)) throw error;
|
|
1223
|
+
this.#sessions.markSessionEnded(session, "Voice Call is no longer active.");
|
|
1224
|
+
return {
|
|
1225
|
+
handled: true,
|
|
1226
|
+
spoken: false
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
session.twilio.introSent = true;
|
|
1230
|
+
session.updatedAt = nowIso();
|
|
1231
|
+
return {
|
|
1232
|
+
handled: true,
|
|
1233
|
+
spoken: true
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
async #captureTranscript(session, options = {}) {
|
|
1237
|
+
const tab = session.chrome?.browserTab;
|
|
1238
|
+
if (!tab) return;
|
|
1239
|
+
return session.transport === "chrome-node" ? await readChromeMeetTranscriptOnNode({
|
|
1240
|
+
runtime: this.params.runtime,
|
|
1241
|
+
nodeId: session.chrome?.nodeId,
|
|
1242
|
+
config: this.params.config,
|
|
1243
|
+
...options.finalize === void 0 ? {} : { finalize: options.finalize },
|
|
1244
|
+
meetingUrl: session.url,
|
|
1245
|
+
meetingSessionId: session.id,
|
|
1246
|
+
tab
|
|
1247
|
+
}) : await readChromeMeetTranscript({
|
|
1248
|
+
runtime: this.params.runtime,
|
|
1249
|
+
config: this.params.config,
|
|
1250
|
+
...options.finalize === void 0 ? {} : { finalize: options.finalize },
|
|
1251
|
+
meetingUrl: session.url,
|
|
1252
|
+
meetingSessionId: session.id,
|
|
1253
|
+
tab
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
async #releaseBrowserTab(session) {
|
|
1257
|
+
if (!isBrowserTransport(session.transport)) return;
|
|
1258
|
+
const tab = session.chrome?.browserTab;
|
|
1259
|
+
if (!tab) {
|
|
1260
|
+
noteSession(session, "No tracked Meet browser tab for this session; close the Meet tab manually if it is still in the call.");
|
|
1261
|
+
session.browserLeft = false;
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
if (this.list().some((other) => other.id !== session.id && other.state === "active" && isBrowserTransport(other.transport) && other.chrome?.browserTab?.targetId === tab.targetId && other.chrome?.nodeId === session.chrome?.nodeId)) {
|
|
1265
|
+
noteSession(session, "Kept the shared Meet tab open because another active session uses it.");
|
|
1266
|
+
session.browserLeft = void 0;
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
let left;
|
|
1270
|
+
try {
|
|
1271
|
+
const result = session.transport === "chrome-node" ? await leaveChromeMeetOnNode({
|
|
1272
|
+
runtime: this.params.runtime,
|
|
1273
|
+
nodeId: session.chrome?.nodeId,
|
|
1274
|
+
config: this.params.config,
|
|
1275
|
+
meetingSessionId: session.id,
|
|
1276
|
+
meetingUrl: session.url,
|
|
1277
|
+
tab
|
|
1278
|
+
}) : await leaveChromeMeet({
|
|
1279
|
+
runtime: this.params.runtime,
|
|
1280
|
+
config: this.params.config,
|
|
1281
|
+
meetingSessionId: session.id,
|
|
1282
|
+
meetingUrl: session.url,
|
|
1283
|
+
tab
|
|
1284
|
+
});
|
|
1285
|
+
noteSession(session, result.note);
|
|
1286
|
+
left = result.left;
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
noteSession(session, `Browser control could not leave the Meet tab: ${formatErrorMessage(error)}`);
|
|
1289
|
+
left = false;
|
|
1290
|
+
}
|
|
1291
|
+
if (session.chrome && left) {
|
|
1292
|
+
session.chrome.browserTab = void 0;
|
|
1293
|
+
if (session.chrome.health) session.chrome.health = {
|
|
1294
|
+
...session.chrome.health,
|
|
1295
|
+
captioning: false,
|
|
1296
|
+
audioOutputRouted: false,
|
|
1297
|
+
providerConnected: false,
|
|
1298
|
+
realtimeReady: false,
|
|
1299
|
+
audioInputActive: false,
|
|
1300
|
+
audioOutputActive: false
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
session.browserLeft = left;
|
|
1304
|
+
return left;
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
//#endregion
|
|
1308
|
+
export { GoogleMeetRuntime };
|