@openclaw/google-meet 2026.7.2-beta.7 → 2026.8.1-beta.2
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-o7RWVqDb.js} +166 -224
- package/dist/{config-BcaS1BDr.js → config-wBK8loBb.js} +97 -221
- package/dist/{create-af1MDlus.js → create-UHeaRzsH.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-CfeT1SYY.js} +335 -355
- package/dist/plugin-registration-rSOssQmm.js +141 -0
- package/dist/runtime-B8d1beHo.js +1308 -0
- package/openclaw.plugin.json +8 -47
- package/package.json +4 -4
|
@@ -0,0 +1,791 @@
|
|
|
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 { a as isSameMeetUrlForReuse, i as isRecoverableMeetTab, n as forceMeetEnglishUi, o as normalizeMeetUrlForReuse, r as isEnglishMeetTab, s as readMeetAuthUser, t as createMeetWithBrowserProxyOnNode } from "./chrome-create-PKAyxCiL.js";
|
|
4
|
+
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
5
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
6
|
+
import { MeetingPlatformAdapter } from "openclaw/plugin-sdk/meeting-runtime";
|
|
7
|
+
//#region extensions/google-meet/src/transports/types.ts
|
|
8
|
+
const GOOGLE_MEET_TRANSCRIPT_MAX_LINES = 2e3;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region extensions/google-meet/src/transports/google-meet-page-scripts.ts
|
|
11
|
+
const GOOGLE_MEET_CAPTION_SETTLE_MS = 1e3;
|
|
12
|
+
function meetStatusScript(params) {
|
|
13
|
+
return `async () => {
|
|
14
|
+
const text = (node) => (node?.innerText || node?.textContent || "").trim();
|
|
15
|
+
const manualActionFor = (reason, message) => ({ reason, message });
|
|
16
|
+
const allowMicrophone = ${JSON.stringify(params.allowMicrophone)};
|
|
17
|
+
const captionSessionId = ${JSON.stringify(params.captionSessionId)};
|
|
18
|
+
const captureCaptions = ${JSON.stringify(params.captureCaptions)};
|
|
19
|
+
const readOnly = ${JSON.stringify(Boolean(params.readOnly))};
|
|
20
|
+
const buttons = () => [...document.querySelectorAll('button')];
|
|
21
|
+
const buttonLabel = (button) =>
|
|
22
|
+
[
|
|
23
|
+
button.getAttribute("aria-label"),
|
|
24
|
+
button.getAttribute("data-tooltip"),
|
|
25
|
+
text(button),
|
|
26
|
+
]
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.join(" ");
|
|
29
|
+
const buttonLabels = buttons().map(buttonLabel).filter(Boolean);
|
|
30
|
+
const notes = [];
|
|
31
|
+
let audioInputRouted;
|
|
32
|
+
let audioInputDeviceLabel;
|
|
33
|
+
let audioInputRouteError;
|
|
34
|
+
let audioOutputRouted;
|
|
35
|
+
let audioOutputDeviceLabel;
|
|
36
|
+
let audioOutputRouteError;
|
|
37
|
+
const findButton = (pattern) =>
|
|
38
|
+
buttons().find((button) => {
|
|
39
|
+
const label = buttonLabel(button);
|
|
40
|
+
return pattern.test(label) && !button.disabled;
|
|
41
|
+
});
|
|
42
|
+
const findCallControlButton = (pattern) =>
|
|
43
|
+
buttons().find((button) => {
|
|
44
|
+
const label = buttonLabel(button);
|
|
45
|
+
return pattern.test(label) && !/remotely mute|someone else/i.test(label) && !button.disabled;
|
|
46
|
+
});
|
|
47
|
+
const audioDeviceFamily = (value) => {
|
|
48
|
+
const label = String(value || '');
|
|
49
|
+
if (/\\bOpenClaw Meeting Audio\\b/i.test(label)) return 'openclaw-meeting-audio';
|
|
50
|
+
if (/\\bBlackHole\\s+2ch\\b/i.test(label)) return 'blackhole-2ch';
|
|
51
|
+
return undefined;
|
|
52
|
+
};
|
|
53
|
+
const isMeetingAudioDevice = (value) => Boolean(audioDeviceFamily(value));
|
|
54
|
+
const deviceNodeLabel = (node) => [
|
|
55
|
+
node?.getAttribute?.('aria-label'),
|
|
56
|
+
node?.getAttribute?.('data-tooltip'),
|
|
57
|
+
node?.getAttribute?.('title'),
|
|
58
|
+
node?.label,
|
|
59
|
+
node?.textContent,
|
|
60
|
+
node?.innerText,
|
|
61
|
+
].filter(Boolean).join(' ').trim();
|
|
62
|
+
const waitForUi = () => new Promise((resolve) => setTimeout(resolve, 100));
|
|
63
|
+
const input = [...document.querySelectorAll('input')].find((el) =>
|
|
64
|
+
/your name/i.test(el.getAttribute('aria-label') || el.placeholder || '')
|
|
65
|
+
);
|
|
66
|
+
if (!readOnly && ${JSON.stringify(params.autoJoin)} && input && !input.value) {
|
|
67
|
+
input.focus();
|
|
68
|
+
input.value = ${JSON.stringify(params.guestName)};
|
|
69
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
70
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
71
|
+
}
|
|
72
|
+
const pageText = text(document.body).toLowerCase();
|
|
73
|
+
const permissionText = [pageText, ...buttonLabels].join("\\n");
|
|
74
|
+
const host = location.hostname.toLowerCase();
|
|
75
|
+
const pageUrl = location.href;
|
|
76
|
+
const permissionNeeded = /permission needed|microphone problem|speaker problem|allow.*(microphone|camera)|blocked.*(microphone|camera)|permission.*(microphone|camera|speaker)/i.test(permissionText);
|
|
77
|
+
let mic = findCallControlButton(/^\\s*turn (?:off|on) microphone\\b/i);
|
|
78
|
+
if (!mic) {
|
|
79
|
+
const callControls = document.querySelector('[role="region"][aria-label="Call controls"]');
|
|
80
|
+
mic = [...(callControls?.querySelectorAll('button') || [])].find((button) =>
|
|
81
|
+
/^\\s*turn (?:off|on) microphone\\b/i.test(buttonLabel(button))
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const joinElsewhere = findButton(/join here too/i);
|
|
85
|
+
const microphoneChoice = findButton(/\\buse microphone\\b/i);
|
|
86
|
+
const noMicrophoneChoice = findButton(/\\b(continue|join|use) without (microphone|mic)\\b|\\bnot now\\b/i);
|
|
87
|
+
if (!readOnly && allowMicrophone && microphoneChoice) {
|
|
88
|
+
microphoneChoice.click();
|
|
89
|
+
notes.push("Accepted Meet microphone prompt with browser automation.");
|
|
90
|
+
await waitForUi();
|
|
91
|
+
} else if (!readOnly && !allowMicrophone && noMicrophoneChoice) {
|
|
92
|
+
noMicrophoneChoice.click();
|
|
93
|
+
notes.push("Skipped Meet microphone prompt for observe-only mode.");
|
|
94
|
+
await waitForUi();
|
|
95
|
+
}
|
|
96
|
+
const findMicrophoneDeviceControl = () => {
|
|
97
|
+
const selectors = [
|
|
98
|
+
'select[aria-label*="microphone" i]',
|
|
99
|
+
'select[name*="microphone" i]',
|
|
100
|
+
'[role="combobox"][aria-label*="microphone" i]',
|
|
101
|
+
'[role="listbox"][aria-label*="microphone" i]',
|
|
102
|
+
'[aria-haspopup="listbox"][aria-label*="microphone" i]',
|
|
103
|
+
];
|
|
104
|
+
for (const selector of selectors) {
|
|
105
|
+
const control = document.querySelector(selector);
|
|
106
|
+
if (control) return control;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
};
|
|
110
|
+
const selectedMicrophoneLabel = () => {
|
|
111
|
+
const control = findMicrophoneDeviceControl();
|
|
112
|
+
const nativeSelected = control?.selectedOptions?.[0];
|
|
113
|
+
if (nativeSelected && isMeetingAudioDevice(deviceNodeLabel(nativeSelected))) {
|
|
114
|
+
return deviceNodeLabel(nativeSelected);
|
|
115
|
+
}
|
|
116
|
+
const controlledId = control?.getAttribute?.('aria-controls');
|
|
117
|
+
const optionsRoot = controlledId ? document.getElementById?.(controlledId) : control;
|
|
118
|
+
const selected = [...(optionsRoot?.querySelectorAll?.(
|
|
119
|
+
'[role="option"][aria-selected="true"], [role="menuitemradio"][aria-checked="true"], [role="radio"][aria-checked="true"]'
|
|
120
|
+
) || [])].find((node) => isMeetingAudioDevice(deviceNodeLabel(node)));
|
|
121
|
+
if (selected) return deviceNodeLabel(selected);
|
|
122
|
+
const role = control?.getAttribute?.('role');
|
|
123
|
+
if (
|
|
124
|
+
!control?.options &&
|
|
125
|
+
role !== 'listbox' &&
|
|
126
|
+
control &&
|
|
127
|
+
isMeetingAudioDevice(deviceNodeLabel(control))
|
|
128
|
+
) {
|
|
129
|
+
return deviceNodeLabel(control);
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
};
|
|
133
|
+
const openMicrophoneDeviceSettings = async () => {
|
|
134
|
+
if (findMicrophoneDeviceControl()) return;
|
|
135
|
+
const candidates = () => [
|
|
136
|
+
...document.querySelectorAll('button, [role="menuitem"], [role="tab"]'),
|
|
137
|
+
];
|
|
138
|
+
const direct = candidates().find((node) =>
|
|
139
|
+
/\\b(?:audio|microphone|device) settings\\b/i.test(deviceNodeLabel(node)) && !node.disabled
|
|
140
|
+
);
|
|
141
|
+
let settings = direct || candidates().find((node) =>
|
|
142
|
+
/^\\s*settings\\s*$/i.test(deviceNodeLabel(node)) && !node.disabled
|
|
143
|
+
);
|
|
144
|
+
if (!settings && !readOnly) {
|
|
145
|
+
const moreOptions = candidates().find((node) =>
|
|
146
|
+
/^\\s*more options\\s*$/i.test(deviceNodeLabel(node)) && !node.disabled
|
|
147
|
+
);
|
|
148
|
+
if (moreOptions) {
|
|
149
|
+
moreOptions.click();
|
|
150
|
+
await waitForUi();
|
|
151
|
+
settings = candidates().find((node) =>
|
|
152
|
+
/^\\s*settings\\s*$/i.test(deviceNodeLabel(node)) && !node.disabled
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!settings || readOnly) return;
|
|
157
|
+
settings.click();
|
|
158
|
+
await waitForUi();
|
|
159
|
+
const audioTab = [...document.querySelectorAll('button, [role="tab"]')].find((node) =>
|
|
160
|
+
/^\\s*audio\\s*$/i.test(deviceNodeLabel(node)) && !node.disabled
|
|
161
|
+
);
|
|
162
|
+
if (audioTab) {
|
|
163
|
+
audioTab.click();
|
|
164
|
+
await waitForUi();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const routeMeetAudioInput = async () => {
|
|
168
|
+
if (
|
|
169
|
+
!allowMicrophone ||
|
|
170
|
+
typeof navigator === 'undefined' ||
|
|
171
|
+
!navigator.mediaDevices?.enumerateDevices
|
|
172
|
+
) return;
|
|
173
|
+
try {
|
|
174
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
175
|
+
const input = devices.find((device) =>
|
|
176
|
+
device.kind === 'audioinput' && isMeetingAudioDevice(device.label)
|
|
177
|
+
);
|
|
178
|
+
if (!input?.deviceId) {
|
|
179
|
+
audioInputRouted = false;
|
|
180
|
+
audioInputRouteError = 'A supported virtual microphone was not visible to Meet.';
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
audioInputDeviceLabel = input.label || 'OpenClaw meeting audio';
|
|
184
|
+
const inputFamily = audioDeviceFamily(audioInputDeviceLabel);
|
|
185
|
+
if (audioDeviceFamily(selectedMicrophoneLabel()) === inputFamily) {
|
|
186
|
+
audioInputRouted = true;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (readOnly) {
|
|
190
|
+
audioInputRouted = false;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
await openMicrophoneDeviceSettings();
|
|
194
|
+
const control = findMicrophoneDeviceControl();
|
|
195
|
+
if (!control) {
|
|
196
|
+
audioInputRouted = false;
|
|
197
|
+
audioInputRouteError = 'Meet microphone device selector was not available.';
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const nativeOptions = [...(control.options || [])];
|
|
201
|
+
const nativeOption = nativeOptions.find(
|
|
202
|
+
(option) => audioDeviceFamily(deviceNodeLabel(option)) === inputFamily
|
|
203
|
+
);
|
|
204
|
+
if (nativeOption) {
|
|
205
|
+
control.value = nativeOption.value;
|
|
206
|
+
nativeOption.selected = true;
|
|
207
|
+
control.dispatchEvent(new Event('input', { bubbles: true }));
|
|
208
|
+
control.dispatchEvent(new Event('change', { bubbles: true }));
|
|
209
|
+
} else {
|
|
210
|
+
control.click?.();
|
|
211
|
+
await waitForUi();
|
|
212
|
+
const choices = [...document.querySelectorAll(
|
|
213
|
+
'[role="option"], [role="menuitemradio"], [role="radio"]'
|
|
214
|
+
)];
|
|
215
|
+
const choice = choices.find(
|
|
216
|
+
(node) => audioDeviceFamily(deviceNodeLabel(node)) === inputFamily && !node.disabled
|
|
217
|
+
);
|
|
218
|
+
choice?.click?.();
|
|
219
|
+
}
|
|
220
|
+
await waitForUi();
|
|
221
|
+
audioInputRouted = audioDeviceFamily(selectedMicrophoneLabel()) === inputFamily;
|
|
222
|
+
if (audioInputRouted) {
|
|
223
|
+
notes.push(\`Selected \${audioInputDeviceLabel} as the Meet microphone.\`);
|
|
224
|
+
} else {
|
|
225
|
+
audioInputRouteError = \`Meet did not confirm \${audioInputDeviceLabel} as its microphone.\`;
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
audioInputRouted = false;
|
|
229
|
+
audioInputRouteError = error?.message || String(error);
|
|
230
|
+
notes.push(\`Could not select the Meet virtual microphone: \${audioInputRouteError}\`);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
await routeMeetAudioInput();
|
|
234
|
+
mic = findCallControlButton(/^\\s*turn (?:off|on) microphone\\b/i) || mic;
|
|
235
|
+
if (
|
|
236
|
+
!readOnly &&
|
|
237
|
+
allowMicrophone &&
|
|
238
|
+
audioInputRouted !== true &&
|
|
239
|
+
mic &&
|
|
240
|
+
/turn off microphone/i.test(buttonLabel(mic))
|
|
241
|
+
) {
|
|
242
|
+
mic.click();
|
|
243
|
+
notes.push("Muted the Meet microphone because the virtual audio input was not verified.");
|
|
244
|
+
mic = findCallControlButton(/^\\s*turn (?:off|on) microphone\\b/i) || mic;
|
|
245
|
+
}
|
|
246
|
+
if (!readOnly && allowMicrophone && audioInputRouted === true && mic && /turn on microphone/i.test(buttonLabel(mic))) {
|
|
247
|
+
mic.click();
|
|
248
|
+
notes.push("Turned on the Meet microphone after verifying the virtual audio input.");
|
|
249
|
+
}
|
|
250
|
+
if (!readOnly && !allowMicrophone && mic && /turn off microphone/i.test(mic.getAttribute('aria-label') || text(mic))) {
|
|
251
|
+
mic.click();
|
|
252
|
+
notes.push("Muted Meet microphone for observe-only mode.");
|
|
253
|
+
}
|
|
254
|
+
const join = !readOnly && ${JSON.stringify(params.autoJoin)}
|
|
255
|
+
? findButton(/join now|ask to join/i)
|
|
256
|
+
: null;
|
|
257
|
+
if (join) join.click();
|
|
258
|
+
const inCall = buttons().some((button) => /leave call/i.test(button.getAttribute('aria-label') || text(button)));
|
|
259
|
+
const routeMeetAudioOutput = async () => {
|
|
260
|
+
if (
|
|
261
|
+
!allowMicrophone ||
|
|
262
|
+
typeof navigator === 'undefined' ||
|
|
263
|
+
!navigator.mediaDevices?.enumerateDevices
|
|
264
|
+
) return;
|
|
265
|
+
const mediaElements = [...document.querySelectorAll('audio, video')]
|
|
266
|
+
.filter((el) => typeof el.setSinkId === 'function');
|
|
267
|
+
if (mediaElements.length === 0) return;
|
|
268
|
+
try {
|
|
269
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
270
|
+
const inputFamily = audioDeviceFamily(audioInputDeviceLabel);
|
|
271
|
+
const output = devices.find((device) =>
|
|
272
|
+
device.kind === 'audiooutput' &&
|
|
273
|
+
inputFamily &&
|
|
274
|
+
audioDeviceFamily(device.label) === inputFamily
|
|
275
|
+
) || devices.find((device) =>
|
|
276
|
+
device.kind === 'audiooutput' && isMeetingAudioDevice(device.label)
|
|
277
|
+
);
|
|
278
|
+
if (!output?.deviceId) {
|
|
279
|
+
audioOutputRouted = false;
|
|
280
|
+
if (devices.some((device) => device.kind === 'audiooutput')) {
|
|
281
|
+
notes.push("A supported virtual speaker output was not visible to Meet.");
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
let routed = 0;
|
|
286
|
+
for (const element of mediaElements) {
|
|
287
|
+
if (element.sinkId !== output.deviceId) {
|
|
288
|
+
if (readOnly) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
await element.setSinkId(output.deviceId);
|
|
292
|
+
routed += 1;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
audioOutputRouted = mediaElements.some((element) => element.sinkId === output.deviceId);
|
|
296
|
+
audioOutputDeviceLabel = output.label || "OpenClaw meeting audio";
|
|
297
|
+
if (!readOnly && audioOutputRouted) {
|
|
298
|
+
notes.push(
|
|
299
|
+
routed > 0
|
|
300
|
+
? \`Routed Meet media output to \${audioOutputDeviceLabel}.\`
|
|
301
|
+
: \`Meet media output already routed to \${audioOutputDeviceLabel}.\`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
audioOutputRouted = false;
|
|
306
|
+
audioOutputRouteError = error?.message || String(error);
|
|
307
|
+
notes.push(\`Could not route Meet speaker output to the virtual audio device: \${audioOutputRouteError}\`);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
if (inCall) {
|
|
311
|
+
await routeMeetAudioOutput();
|
|
312
|
+
}
|
|
313
|
+
let captioning = false;
|
|
314
|
+
let captionsEnabledAttempted = false;
|
|
315
|
+
let transcriptLines = 0;
|
|
316
|
+
let lastCaptionAt;
|
|
317
|
+
let lastCaptionSpeaker;
|
|
318
|
+
let lastCaptionText;
|
|
319
|
+
let recentTranscript = [];
|
|
320
|
+
const captionSelector = '[role="region"][aria-label*="aption" i], [aria-live="polite"][role="region"], div[aria-live="polite"]';
|
|
321
|
+
const captionState = (() => {
|
|
322
|
+
if (!captureCaptions) return undefined;
|
|
323
|
+
const w = window;
|
|
324
|
+
if (!inCall && !w.__openclawMeetCaptions) return undefined;
|
|
325
|
+
// A reused tab starts a fresh logical transcript for each OpenClaw session.
|
|
326
|
+
// Status refreshes omit the id, so they preserve the active page-owned buffer.
|
|
327
|
+
if (!w.__openclawMeetCaptions || (captionSessionId && w.__openclawMeetCaptions.sessionId !== captionSessionId)) {
|
|
328
|
+
if (w.__openclawMeetCaptions?.settleTimer !== undefined) {
|
|
329
|
+
clearTimeout(w.__openclawMeetCaptions.settleTimer);
|
|
330
|
+
}
|
|
331
|
+
w.__openclawMeetCaptions?.observer?.disconnect?.();
|
|
332
|
+
w.__openclawMeetCaptions = {
|
|
333
|
+
sessionId: captionSessionId,
|
|
334
|
+
// Epochs cross document lifetimes in the runtime transcript cursor.
|
|
335
|
+
// Strong UUIDs keep a reloaded page distinct from its prior buffer.
|
|
336
|
+
epoch: crypto.randomUUID(),
|
|
337
|
+
enabledAttempted: false,
|
|
338
|
+
observerInstalled: false,
|
|
339
|
+
observer: undefined,
|
|
340
|
+
droppedLines: 0,
|
|
341
|
+
lines: [],
|
|
342
|
+
settleTimer: undefined,
|
|
343
|
+
visible: []
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return w.__openclawMeetCaptions;
|
|
347
|
+
})();
|
|
348
|
+
const normalizeCaption = (speaker, captionText) => {
|
|
349
|
+
if (!captionState) return;
|
|
350
|
+
const clean = String(captionText || "").replace(/\\s+/g, " ").trim();
|
|
351
|
+
const cleanSpeaker = String(speaker || "").replace(/\\s+/g, " ").trim();
|
|
352
|
+
if (!clean || clean.length < 2) return undefined;
|
|
353
|
+
if (/^(turn on captions|turn off captions|captions)$/i.test(clean)) return undefined;
|
|
354
|
+
return { speaker: cleanSpeaker || undefined, text: clean };
|
|
355
|
+
};
|
|
356
|
+
const commitLines = (state, entries) => {
|
|
357
|
+
state.lines.push(...entries.map((entry) => ({
|
|
358
|
+
at: entry.at,
|
|
359
|
+
speaker: entry.speaker,
|
|
360
|
+
text: entry.text
|
|
361
|
+
})));
|
|
362
|
+
const excess = state.lines.length - ${GOOGLE_MEET_TRANSCRIPT_MAX_LINES};
|
|
363
|
+
if (excess > 0) {
|
|
364
|
+
state.lines.splice(0, excess);
|
|
365
|
+
state.droppedLines = (state.droppedLines || 0) + excess;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const scrapeCaptions = () => {
|
|
369
|
+
if (!captionState) return;
|
|
370
|
+
const regions = [...document.querySelectorAll(captionSelector)];
|
|
371
|
+
const rows = [];
|
|
372
|
+
for (const region of regions) {
|
|
373
|
+
const raw = text(region);
|
|
374
|
+
if (!raw) continue;
|
|
375
|
+
const pieces = raw.split(/\\n+/).map((part) => part.trim()).filter(Boolean);
|
|
376
|
+
const row = pieces.length >= 2
|
|
377
|
+
? normalizeCaption(pieces[0], pieces.slice(1).join(" "))
|
|
378
|
+
: normalizeCaption("", pieces[0] || raw);
|
|
379
|
+
if (row) rows.push({ ...row, node: region });
|
|
380
|
+
}
|
|
381
|
+
if (rows.length === 0) {
|
|
382
|
+
// Meet briefly removes caption rows while rerendering. Keep them mutable
|
|
383
|
+
// for one settle window so a DOM gap cannot fabricate a repeated line.
|
|
384
|
+
if (captionState.visible.length > 0 && captionState.settleTimer === undefined) {
|
|
385
|
+
const pendingState = captionState;
|
|
386
|
+
pendingState.settleTimer = setTimeout(() => {
|
|
387
|
+
if (window.__openclawMeetCaptions !== pendingState) return;
|
|
388
|
+
commitLines(pendingState, pendingState.visible);
|
|
389
|
+
pendingState.visible = [];
|
|
390
|
+
pendingState.settleTimer = undefined;
|
|
391
|
+
}, ${GOOGLE_MEET_CAPTION_SETTLE_MS});
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (captionState.settleTimer !== undefined) {
|
|
396
|
+
clearTimeout(captionState.settleTimer);
|
|
397
|
+
captionState.settleTimer = undefined;
|
|
398
|
+
}
|
|
399
|
+
const previous = Array.isArray(captionState.visible) ? captionState.visible : [];
|
|
400
|
+
const unmatchedPrevious = [...previous];
|
|
401
|
+
const nextVisible = [];
|
|
402
|
+
const now = Date.now();
|
|
403
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
404
|
+
const row = rows[index];
|
|
405
|
+
const priorIndex = unmatchedPrevious.findIndex((candidate) => {
|
|
406
|
+
const sameTextLifecycle =
|
|
407
|
+
candidate.text === row.text ||
|
|
408
|
+
row.text.startsWith(candidate.text) ||
|
|
409
|
+
candidate.text.startsWith(row.text);
|
|
410
|
+
const sameDomLifecycle =
|
|
411
|
+
candidate.node === row.node || now - candidate.seenAt <= ${GOOGLE_MEET_CAPTION_SETTLE_MS};
|
|
412
|
+
return candidate.speaker === row.speaker && sameTextLifecycle && sameDomLifecycle;
|
|
413
|
+
});
|
|
414
|
+
const prior = priorIndex >= 0 ? unmatchedPrevious.splice(priorIndex, 1)[0] : undefined;
|
|
415
|
+
const sameSpeaker = Boolean(prior) && prior.speaker === row.speaker;
|
|
416
|
+
if (sameSpeaker && prior.text === row.text) {
|
|
417
|
+
prior.node = row.node;
|
|
418
|
+
prior.seenAt = now;
|
|
419
|
+
nextVisible.push(prior);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (sameSpeaker && row.text.startsWith(prior.text)) {
|
|
423
|
+
prior.text = row.text;
|
|
424
|
+
prior.node = row.node;
|
|
425
|
+
prior.seenAt = now;
|
|
426
|
+
nextVisible.push(prior);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (sameSpeaker && prior.text.startsWith(row.text)) {
|
|
430
|
+
prior.node = row.node;
|
|
431
|
+
prior.seenAt = now;
|
|
432
|
+
nextVisible.push(prior);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const entry = {
|
|
436
|
+
at: new Date().toISOString(),
|
|
437
|
+
node: row.node,
|
|
438
|
+
seenAt: now,
|
|
439
|
+
speaker: row.speaker,
|
|
440
|
+
text: row.text
|
|
441
|
+
};
|
|
442
|
+
nextVisible.push(entry);
|
|
443
|
+
}
|
|
444
|
+
commitLines(captionState, unmatchedPrevious);
|
|
445
|
+
captionState.visible = nextVisible;
|
|
446
|
+
};
|
|
447
|
+
if (captionState) {
|
|
448
|
+
if (!readOnly && inCall && !captionState.enabledAttempted) {
|
|
449
|
+
const captionButton = findButton(/turn on captions|show captions|captions/i);
|
|
450
|
+
const captionLabel = captionButton ? (captionButton.getAttribute("aria-label") || captionButton.getAttribute("data-tooltip") || text(captionButton)) : "";
|
|
451
|
+
if (captionButton) {
|
|
452
|
+
captionState.enabledAttempted = true;
|
|
453
|
+
captionsEnabledAttempted = true;
|
|
454
|
+
if (!/turn off captions|hide captions/i.test(captionLabel)) {
|
|
455
|
+
captionButton.click();
|
|
456
|
+
notes.push("Attempted to enable Meet captions for observe-only transcript health.");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} else if (captionState.enabledAttempted) {
|
|
460
|
+
captionsEnabledAttempted = true;
|
|
461
|
+
}
|
|
462
|
+
if (inCall && !captionState.observerInstalled) {
|
|
463
|
+
captionState.observerInstalled = true;
|
|
464
|
+
captionState.observer = new MutationObserver(scrapeCaptions);
|
|
465
|
+
captionState.observer.observe(document.body, {
|
|
466
|
+
childList: true,
|
|
467
|
+
subtree: true,
|
|
468
|
+
characterData: true
|
|
469
|
+
});
|
|
470
|
+
notes.push("Installed Meet caption observer for observe-only transcript health.");
|
|
471
|
+
}
|
|
472
|
+
if (inCall) {
|
|
473
|
+
scrapeCaptions();
|
|
474
|
+
}
|
|
475
|
+
const committedLines = Array.isArray(captionState.lines) ? captionState.lines : [];
|
|
476
|
+
const visibleLines = Array.isArray(captionState.visible) ? captionState.visible : [];
|
|
477
|
+
const lines = [...committedLines, ...visibleLines];
|
|
478
|
+
const last = lines[lines.length - 1];
|
|
479
|
+
captioning = document.querySelector(captionSelector) !== null || lines.length > 0;
|
|
480
|
+
transcriptLines = (captionState.droppedLines || 0) + lines.length;
|
|
481
|
+
lastCaptionAt = last?.at;
|
|
482
|
+
lastCaptionSpeaker = last?.speaker;
|
|
483
|
+
lastCaptionText = last?.text;
|
|
484
|
+
recentTranscript = lines.slice(-5);
|
|
485
|
+
}
|
|
486
|
+
const lobbyWaiting = !inCall && /asking to be let in|you.?ll join when someone lets you in|waiting to be let in|ask to join/i.test(pageText);
|
|
487
|
+
const leaveReason = !inCall && /you left the meeting|you.?ve left the meeting|removed from the meeting|you were removed|call ended|meeting ended/i.test(pageText)
|
|
488
|
+
? pageText.match(/you left the meeting|you.?ve left the meeting|removed from the meeting|you were removed|call ended|meeting ended/i)?.[0]
|
|
489
|
+
: undefined;
|
|
490
|
+
let manualAction;
|
|
491
|
+
if (!inCall && (host === "accounts.google.com" || /use your google account|to continue to google meet|choose an account|sign in to (join|continue)/i.test(pageText))) {
|
|
492
|
+
manualAction = manualActionFor("google-login-required", "Sign in to Google in the OpenClaw browser profile, then retry the Meet join.");
|
|
493
|
+
} else if (!inCall && joinElsewhere) {
|
|
494
|
+
manualAction = manualActionFor("meet-session-conflict", "Meet is already active in another tab or device. Leave that session or reuse an English-pinned tab before retrying.");
|
|
495
|
+
} else if (!inCall && /asking to be let in|you.?ll join when someone lets you in|waiting to be let in|ask to join/i.test(pageText)) {
|
|
496
|
+
manualAction = manualActionFor("meet-admission-required", "Admit the OpenClaw browser participant in Google Meet, then retry speech.");
|
|
497
|
+
} else if (permissionNeeded) {
|
|
498
|
+
manualAction = manualActionFor("meet-permission-required", allowMicrophone ? "Allow microphone/camera/speaker permissions for Meet in the OpenClaw browser profile, then retry." : "Join without microphone/camera permissions in the OpenClaw browser profile, then retry.");
|
|
499
|
+
} else if (inCall && allowMicrophone && (audioInputRouted !== true || audioOutputRouted !== true)) {
|
|
500
|
+
manualAction = manualActionFor(
|
|
501
|
+
"meet-audio-choice-required",
|
|
502
|
+
"Select BlackHole 2ch or OpenClaw Meeting Audio as both the Meet microphone and speaker, then retry."
|
|
503
|
+
);
|
|
504
|
+
} else if (!inCall && (allowMicrophone ? !microphoneChoice : !noMicrophoneChoice) && /do you want people to hear you in the meeting/i.test(pageText)) {
|
|
505
|
+
manualAction = manualActionFor("meet-audio-choice-required", allowMicrophone ? "Meet is showing the microphone choice. Click Use microphone in the OpenClaw browser profile, then retry." : "Meet is showing the microphone choice. Choose the no-microphone option in the OpenClaw browser profile, then retry.");
|
|
506
|
+
}
|
|
507
|
+
return JSON.stringify({
|
|
508
|
+
clickedJoin: Boolean(join),
|
|
509
|
+
clickedMicrophoneChoice: Boolean(allowMicrophone && microphoneChoice),
|
|
510
|
+
inCall,
|
|
511
|
+
micMuted: mic ? /turn on microphone/i.test(buttonLabel(mic)) : undefined,
|
|
512
|
+
lobbyWaiting,
|
|
513
|
+
leaveReason,
|
|
514
|
+
captioning,
|
|
515
|
+
captionsEnabledAttempted,
|
|
516
|
+
transcriptLines,
|
|
517
|
+
lastCaptionAt,
|
|
518
|
+
lastCaptionSpeaker,
|
|
519
|
+
lastCaptionText,
|
|
520
|
+
recentTranscript,
|
|
521
|
+
audioInputRouted,
|
|
522
|
+
audioInputDeviceLabel,
|
|
523
|
+
audioInputRouteError,
|
|
524
|
+
audioOutputRouted,
|
|
525
|
+
audioOutputDeviceLabel,
|
|
526
|
+
audioOutputRouteError,
|
|
527
|
+
manualAction,
|
|
528
|
+
title: document.title,
|
|
529
|
+
url: pageUrl,
|
|
530
|
+
notes
|
|
531
|
+
});
|
|
532
|
+
}`;
|
|
533
|
+
}
|
|
534
|
+
function meetTranscriptScript(meetingUrl, meetingSessionId, finalize) {
|
|
535
|
+
const expectedMeetingUrl = normalizeMeetUrlForReuse(meetingUrl);
|
|
536
|
+
return `() => {
|
|
537
|
+
const expectedMeetingUrl = ${JSON.stringify(expectedMeetingUrl)};
|
|
538
|
+
const expectedSessionId = ${JSON.stringify(meetingSessionId)};
|
|
539
|
+
let currentMeetingUrl;
|
|
540
|
+
try {
|
|
541
|
+
const currentUrl = new URL(location.href);
|
|
542
|
+
currentMeetingUrl = currentUrl.origin + currentUrl.pathname.toLowerCase().replace(/\\/$/, "");
|
|
543
|
+
} catch {
|
|
544
|
+
return JSON.stringify({ urlMatched: false });
|
|
545
|
+
}
|
|
546
|
+
if (!expectedMeetingUrl || currentMeetingUrl !== expectedMeetingUrl) {
|
|
547
|
+
return JSON.stringify({ urlMatched: false });
|
|
548
|
+
}
|
|
549
|
+
const state = window.__openclawMeetCaptions;
|
|
550
|
+
if (state?.sessionId && state.sessionId !== expectedSessionId) {
|
|
551
|
+
return JSON.stringify({ urlMatched: true, sessionMatched: false });
|
|
552
|
+
}
|
|
553
|
+
if (${JSON.stringify(finalize)} && Array.isArray(state?.visible) && state.visible.length > 0) {
|
|
554
|
+
if (state.settleTimer !== undefined) clearTimeout(state.settleTimer);
|
|
555
|
+
state.settleTimer = undefined;
|
|
556
|
+
state.lines = Array.isArray(state.lines) ? state.lines : [];
|
|
557
|
+
state.lines.push(...state.visible.map((entry) => ({
|
|
558
|
+
at: entry.at,
|
|
559
|
+
speaker: entry.speaker,
|
|
560
|
+
text: entry.text
|
|
561
|
+
})));
|
|
562
|
+
state.visible = [];
|
|
563
|
+
const excess = state.lines.length - ${GOOGLE_MEET_TRANSCRIPT_MAX_LINES};
|
|
564
|
+
if (excess > 0) {
|
|
565
|
+
state.lines.splice(0, excess);
|
|
566
|
+
state.droppedLines = (state.droppedLines || 0) + excess;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const lines = Array.isArray(state?.lines) ? state.lines : [];
|
|
570
|
+
return JSON.stringify({
|
|
571
|
+
urlMatched: true,
|
|
572
|
+
sessionMatched: true,
|
|
573
|
+
epoch: typeof state?.epoch === "string" ? state.epoch : undefined,
|
|
574
|
+
droppedLines: Number.isFinite(state?.droppedLines) ? Math.max(0, Math.trunc(state.droppedLines)) : 0,
|
|
575
|
+
lines: lines.map((line) => ({
|
|
576
|
+
at: typeof line?.at === "string" ? line.at : undefined,
|
|
577
|
+
speaker: typeof line?.speaker === "string" ? line.speaker : undefined,
|
|
578
|
+
text: typeof line?.text === "string" ? line.text : ""
|
|
579
|
+
})).filter((line) => line.text)
|
|
580
|
+
});
|
|
581
|
+
}`;
|
|
582
|
+
}
|
|
583
|
+
function meetLeaveScript(meetingUrl) {
|
|
584
|
+
const expectedMeetingUrl = normalizeMeetUrlForReuse(meetingUrl);
|
|
585
|
+
return `() => {
|
|
586
|
+
const expectedMeetingUrl = ${JSON.stringify(expectedMeetingUrl)};
|
|
587
|
+
let currentMeetingUrl;
|
|
588
|
+
try {
|
|
589
|
+
const currentUrl = new URL(location.href);
|
|
590
|
+
currentMeetingUrl = currentUrl.origin + currentUrl.pathname.toLowerCase().replace(/\\/$/, "");
|
|
591
|
+
} catch {
|
|
592
|
+
return JSON.stringify({ departed: false });
|
|
593
|
+
}
|
|
594
|
+
if (!expectedMeetingUrl) {
|
|
595
|
+
return JSON.stringify({ departed: false });
|
|
596
|
+
}
|
|
597
|
+
if (currentMeetingUrl !== expectedMeetingUrl) {
|
|
598
|
+
return JSON.stringify({ departed: true, urlMatched: false });
|
|
599
|
+
}
|
|
600
|
+
const text = (node) => (node?.innerText || node?.textContent || "").trim();
|
|
601
|
+
// Locale-independent fallback: Meet renders the leave control as a Material
|
|
602
|
+
// Symbols icon whose ligature text is "call_end" in every UI language, so a
|
|
603
|
+
// localized aria-label (e.g. "Anruf verlassen") still resolves to the button.
|
|
604
|
+
const hasLeaveIcon = (button) => {
|
|
605
|
+
const icon = button.querySelector ? button.querySelector("i") : null;
|
|
606
|
+
return icon ? (icon.textContent || "").trim() === "call_end" : false;
|
|
607
|
+
};
|
|
608
|
+
const buttons = [...document.querySelectorAll('button')];
|
|
609
|
+
const label = (button) => [
|
|
610
|
+
button.getAttribute("aria-label"),
|
|
611
|
+
button.getAttribute("data-tooltip"),
|
|
612
|
+
text(button),
|
|
613
|
+
]
|
|
614
|
+
.filter(Boolean)
|
|
615
|
+
.join(" ");
|
|
616
|
+
const postCall = buttons.some((button) => /\\b(rejoin|return to home screen)\\b/i.test(label(button)));
|
|
617
|
+
if (postCall) {
|
|
618
|
+
return JSON.stringify({ departed: true, urlMatched: true });
|
|
619
|
+
}
|
|
620
|
+
// Managed join tabs are reused only after the English-tab gate or opened
|
|
621
|
+
// through the English-UI helper, so follow-up labels are pinned to English.
|
|
622
|
+
const confirmation = buttons.find((button) => {
|
|
623
|
+
return !button.disabled && /\\bleave meeting\\b/i.test(label(button));
|
|
624
|
+
});
|
|
625
|
+
if (confirmation) {
|
|
626
|
+
confirmation.click();
|
|
627
|
+
return JSON.stringify({ departed: false, leaveAction: "confirm", urlMatched: true });
|
|
628
|
+
}
|
|
629
|
+
const leave = buttons.find((button) => {
|
|
630
|
+
if (button.disabled) return false;
|
|
631
|
+
return /leave call/i.test(label(button)) || hasLeaveIcon(button);
|
|
632
|
+
});
|
|
633
|
+
if (leave) {
|
|
634
|
+
leave.click();
|
|
635
|
+
return JSON.stringify({ departed: false, leaveAction: "leave", urlMatched: true });
|
|
636
|
+
}
|
|
637
|
+
return JSON.stringify({ departed: false, urlMatched: true });
|
|
638
|
+
}`;
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region extensions/google-meet/src/transports/twilio.ts
|
|
642
|
+
const DTMF_PATTERN = /^[0-9*#wWpP,]+$/;
|
|
643
|
+
function normalizeDialInNumber(value) {
|
|
644
|
+
const normalized = normalizeOptionalString(value);
|
|
645
|
+
if (!normalized) return;
|
|
646
|
+
const compact = normalized.replace(/[()\s.-]/g, "");
|
|
647
|
+
if (!/^\+?[0-9]{5,20}$/.test(compact)) throw new Error("dialInNumber must be a phone number");
|
|
648
|
+
return compact;
|
|
649
|
+
}
|
|
650
|
+
function normalizeDtmfSequence(value) {
|
|
651
|
+
const normalized = normalizeOptionalString(value);
|
|
652
|
+
if (!normalized) return;
|
|
653
|
+
const compact = normalized.replace(/\s+/g, "");
|
|
654
|
+
if (!DTMF_PATTERN.test(compact)) throw new Error("dtmfSequence may only contain digits, *, #, comma, w, p");
|
|
655
|
+
return compact;
|
|
656
|
+
}
|
|
657
|
+
function buildMeetDtmfSequence(params) {
|
|
658
|
+
const explicit = normalizeDtmfSequence(params.dtmfSequence);
|
|
659
|
+
if (explicit) return explicit;
|
|
660
|
+
const pin = normalizeOptionalString(params.pin);
|
|
661
|
+
if (!pin) return;
|
|
662
|
+
const compactPin = pin.replace(/\s+/g, "");
|
|
663
|
+
if (!/^[0-9]+#?$/.test(compactPin)) throw new Error("pin may only contain digits and an optional trailing #");
|
|
664
|
+
return compactPin.endsWith("#") ? compactPin : `${compactPin}#`;
|
|
665
|
+
}
|
|
666
|
+
function prefixDtmfWait(sequence, delayMs) {
|
|
667
|
+
if (!sequence || delayMs <= 0) return sequence;
|
|
668
|
+
const waitCount = Math.ceil(delayMs / 500);
|
|
669
|
+
if (waitCount <= 0) return sequence;
|
|
670
|
+
return `${"w".repeat(waitCount)}${sequence}`;
|
|
671
|
+
}
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region extensions/google-meet/src/transports/google-meet-platform-adapter.ts
|
|
674
|
+
function parsePermissionGrantNotes(result) {
|
|
675
|
+
const record = result && typeof result === "object" ? result : {};
|
|
676
|
+
const unsupportedPermissions = Array.isArray(record.unsupportedPermissions) ? record.unsupportedPermissions.filter((value) => typeof value === "string") : [];
|
|
677
|
+
const notes = ["Granted Meet microphone/camera permissions through browser control."];
|
|
678
|
+
if (unsupportedPermissions.includes("speakerSelection")) notes.push("Chrome did not accept the optional Meet speaker-selection permission.");
|
|
679
|
+
return notes;
|
|
680
|
+
}
|
|
681
|
+
const manualActionCategories = /* @__PURE__ */ new Map([
|
|
682
|
+
["browser-control-unavailable", "browser-control-unavailable"],
|
|
683
|
+
["google-login-required", "login-required"],
|
|
684
|
+
["meet-admission-required", "admission-required"],
|
|
685
|
+
["meet-audio-choice-required", "audio-choice-required"],
|
|
686
|
+
["meet-locale-required", "locale-required"],
|
|
687
|
+
["meet-permission-required", "permission-required"],
|
|
688
|
+
["meet-session-conflict", "session-conflict"]
|
|
689
|
+
]);
|
|
690
|
+
function classifyManualActionReason(reason) {
|
|
691
|
+
return manualActionCategories.get(reason) ?? "custom";
|
|
692
|
+
}
|
|
693
|
+
const GOOGLE_MEET_PLATFORM_ADAPTER = MeetingPlatformAdapter.create({
|
|
694
|
+
id: "google-meet",
|
|
695
|
+
displayName: "Google Meet",
|
|
696
|
+
browserLabel: "Meet",
|
|
697
|
+
logScope: "[google-meet]",
|
|
698
|
+
agentConsult: {
|
|
699
|
+
surface: "a private Google Meet",
|
|
700
|
+
userLabel: "Participant",
|
|
701
|
+
assistantLabel: "Agent",
|
|
702
|
+
questionSourceLabel: "participant",
|
|
703
|
+
workingResponseLabel: "participant",
|
|
704
|
+
extraSystemPrompt: [
|
|
705
|
+
"You are a behind-the-scenes consultant for a live meeting voice agent.",
|
|
706
|
+
"Prioritize a fast, speakable answer over exhaustive investigation.",
|
|
707
|
+
"For tool-backed status checks, prefer one or two bounded read-only queries before answering.",
|
|
708
|
+
"Do not print secret values or dump environment variables; only check whether required configuration is present.",
|
|
709
|
+
"Be accurate, brief, and speakable."
|
|
710
|
+
].join(" ")
|
|
711
|
+
},
|
|
712
|
+
session: {
|
|
713
|
+
idPrefix: "meet",
|
|
714
|
+
participantIdentity: (transport) => transport === "twilio" ? "Twilio phone participant" : transport === "chrome-node" ? "signed-in Google Chrome profile on a paired node" : "signed-in Google Chrome profile"
|
|
715
|
+
},
|
|
716
|
+
nodeCommandName: GOOGLE_MEET_NODE_COMMAND,
|
|
717
|
+
nodeConfigPath: "plugins.entries.google-meet.config.chromeNode.node",
|
|
718
|
+
urls: {
|
|
719
|
+
validateAndNormalize: normalizeMeetUrl,
|
|
720
|
+
normalizeForReuse: normalizeMeetUrlForReuse,
|
|
721
|
+
isSameMeeting: isSameMeetUrlForReuse,
|
|
722
|
+
buildJoinUrl: (session) => forceMeetEnglishUi(session.url),
|
|
723
|
+
accountHint: readMeetAuthUser,
|
|
724
|
+
isPreferredJoinUrl: isEnglishMeetTab,
|
|
725
|
+
isRecoverableTab: isRecoverableMeetTab,
|
|
726
|
+
localeAction: (tab) => {
|
|
727
|
+
if (!normalizeMeetUrlForReuse(tab.url) || isEnglishMeetTab(tab.url)) return;
|
|
728
|
+
return {
|
|
729
|
+
category: "locale-required",
|
|
730
|
+
reason: "meet-locale-required",
|
|
731
|
+
message: "The existing Meet tab is not pinned to English. Open the meeting with ?hl=en, then retry recovery."
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
browser: {
|
|
736
|
+
allowsMicrophone: MeetingPlatformAdapter.isTalkBackMode,
|
|
737
|
+
buildStatusJoinScript: (params) => meetStatusScript({
|
|
738
|
+
allowMicrophone: MeetingPlatformAdapter.isTalkBackMode(params.mode),
|
|
739
|
+
autoJoin: params.autoJoin,
|
|
740
|
+
captionSessionId: params.meetingSessionId || void 0,
|
|
741
|
+
captureCaptions: params.captureCaptions,
|
|
742
|
+
guestName: params.guestName,
|
|
743
|
+
readOnly: params.readOnly
|
|
744
|
+
}),
|
|
745
|
+
shouldRetryJoinStatus: (health) => health.inCall === true && health.manualAction?.reason === "meet-audio-choice-required" && (health.audioInputRouted !== true || health.audioOutputRouted !== true),
|
|
746
|
+
browserControlUnavailable: () => ({
|
|
747
|
+
category: "browser-control-unavailable",
|
|
748
|
+
reason: "browser-control-unavailable",
|
|
749
|
+
message: "Open the OpenClaw browser profile, finish Google Meet login, admission, or permission prompts, then retry."
|
|
750
|
+
}),
|
|
751
|
+
buildLeaveScript: meetLeaveScript,
|
|
752
|
+
captions: {
|
|
753
|
+
enabled: (_mode) => true,
|
|
754
|
+
buildTranscriptScript: ({ finalize, meetingSessionId, meetingUrl }) => meetTranscriptScript(meetingUrl, meetingSessionId, finalize)
|
|
755
|
+
},
|
|
756
|
+
permissions: ({ allowMicrophone }) => allowMicrophone ? {
|
|
757
|
+
origin: "https://meet.google.com",
|
|
758
|
+
permissions: ["audioCapture", "videoCapture"],
|
|
759
|
+
optionalPermissions: ["speakerSelection"]
|
|
760
|
+
} : void 0,
|
|
761
|
+
permissionNotes: ({ allowMicrophone, error, result }) => {
|
|
762
|
+
if (!allowMicrophone) return ["Observe-only mode skips Meet microphone/camera permission grants."];
|
|
763
|
+
if (error) return [`Could not grant Meet media permissions automatically: ${formatErrorMessage(error)}`];
|
|
764
|
+
return parsePermissionGrantNotes(result);
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
create: { browser: createMeetWithBrowserProxyOnNode },
|
|
768
|
+
dialIn: { buildPlan: (params) => {
|
|
769
|
+
const number = normalizeDialInNumber(params.dialInNumber ?? params.defaultDialInNumber);
|
|
770
|
+
const pin = params.pin ?? params.defaultPin;
|
|
771
|
+
const rawDtmfSequence = buildMeetDtmfSequence({
|
|
772
|
+
pin,
|
|
773
|
+
dtmfSequence: params.dtmfSequence ?? params.defaultDtmfSequence
|
|
774
|
+
});
|
|
775
|
+
return {
|
|
776
|
+
number,
|
|
777
|
+
pin,
|
|
778
|
+
dtmfSequence: params.dtmfSequence || params.defaultDtmfSequence ? rawDtmfSequence : prefixDtmfWait(rawDtmfSequence, params.dtmfDelayMs)
|
|
779
|
+
};
|
|
780
|
+
} },
|
|
781
|
+
parsing: {
|
|
782
|
+
classifyManualActionReason,
|
|
783
|
+
displayName: "Meet",
|
|
784
|
+
invalidTranscriptMessage: "Google Meet transcript payload is invalid.",
|
|
785
|
+
malformedStatusMessage: "Google Meet browser status JSON is malformed.",
|
|
786
|
+
malformedTranscriptMessage: "Google Meet transcript JSON is malformed.",
|
|
787
|
+
statusFields: (parsed) => ({ leaveReason: typeof parsed.leaveReason === "string" ? parsed.leaveReason : void 0 })
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
//#endregion
|
|
791
|
+
export { normalizeDialInNumber as n, GOOGLE_MEET_PLATFORM_ADAPTER as t };
|