@openclaw/google-meet 2026.7.2-beta.5 → 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-GeuAaEIy.js → cli-o7RWVqDb.js} +166 -234
- 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
- /package/node_modules/{core-util-is → jszip/node_modules/core-util-is}/LICENSE +0 -0
- /package/node_modules/{core-util-is → jszip/node_modules/core-util-is}/README.md +0 -0
- /package/node_modules/{core-util-is → jszip/node_modules/core-util-is}/lib/util.js +0 -0
- /package/node_modules/{core-util-is → jszip/node_modules/core-util-is}/package.json +0 -0
- /package/node_modules/{immediate → jszip/node_modules/immediate}/LICENSE.txt +0 -0
- /package/node_modules/{immediate → jszip/node_modules/immediate}/README.md +0 -0
- /package/node_modules/{immediate → jszip/node_modules/immediate}/dist/immediate.js +0 -0
- /package/node_modules/{immediate → jszip/node_modules/immediate}/dist/immediate.min.js +0 -0
- /package/node_modules/{immediate → jszip/node_modules/immediate}/lib/browser.js +0 -0
- /package/node_modules/{immediate → jszip/node_modules/immediate}/lib/index.js +0 -0
- /package/node_modules/{immediate → jszip/node_modules/immediate}/package.json +0 -0
- /package/node_modules/{inherits → jszip/node_modules/inherits}/LICENSE +0 -0
- /package/node_modules/{inherits → jszip/node_modules/inherits}/README.md +0 -0
- /package/node_modules/{inherits → jszip/node_modules/inherits}/inherits.js +0 -0
- /package/node_modules/{inherits → jszip/node_modules/inherits}/inherits_browser.js +0 -0
- /package/node_modules/{inherits → jszip/node_modules/inherits}/package.json +0 -0
- /package/node_modules/{isarray → jszip/node_modules/isarray}/.npmignore +0 -0
- /package/node_modules/{isarray → jszip/node_modules/isarray}/.travis.yml +0 -0
- /package/node_modules/{isarray → jszip/node_modules/isarray}/Makefile +0 -0
- /package/node_modules/{isarray → jszip/node_modules/isarray}/README.md +0 -0
- /package/node_modules/{isarray → jszip/node_modules/isarray}/component.json +0 -0
- /package/node_modules/{isarray → jszip/node_modules/isarray}/index.js +0 -0
- /package/node_modules/{isarray → jszip/node_modules/isarray}/package.json +0 -0
- /package/node_modules/{isarray → jszip/node_modules/isarray}/test.js +0 -0
- /package/node_modules/{lie → jszip/node_modules/lie}/README.md +0 -0
- /package/node_modules/{lie → jszip/node_modules/lie}/dist/lie.js +0 -0
- /package/node_modules/{lie → jszip/node_modules/lie}/dist/lie.min.js +0 -0
- /package/node_modules/{lie → jszip/node_modules/lie}/dist/lie.polyfill.js +0 -0
- /package/node_modules/{lie → jszip/node_modules/lie}/dist/lie.polyfill.min.js +0 -0
- /package/node_modules/{lie → jszip/node_modules/lie}/lib/browser.js +0 -0
- /package/node_modules/{lie → jszip/node_modules/lie}/lib/index.js +0 -0
- /package/node_modules/{lie → jszip/node_modules/lie}/license.md +0 -0
- /package/node_modules/{lie → jszip/node_modules/lie}/lie.d.ts +0 -0
- /package/node_modules/{lie → jszip/node_modules/lie}/package.json +0 -0
- /package/node_modules/{lie → jszip/node_modules/lie}/polyfill.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/CHANGELOG.md +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/LICENSE +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/README.md +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/dist/pako.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/dist/pako.min.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/dist/pako_deflate.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/dist/pako_deflate.min.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/dist/pako_inflate.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/dist/pako_inflate.min.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/index.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/deflate.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/inflate.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/utils/common.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/utils/strings.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/README +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/adler32.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/constants.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/crc32.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/deflate.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/gzheader.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/inffast.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/inflate.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/inftrees.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/messages.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/trees.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/zstream.js +0 -0
- /package/node_modules/{pako → jszip/node_modules/pako}/package.json +0 -0
- /package/node_modules/{process-nextick-args → jszip/node_modules/process-nextick-args}/index.js +0 -0
- /package/node_modules/{process-nextick-args → jszip/node_modules/process-nextick-args}/license.md +0 -0
- /package/node_modules/{process-nextick-args → jszip/node_modules/process-nextick-args}/package.json +0 -0
- /package/node_modules/{process-nextick-args → jszip/node_modules/process-nextick-args}/readme.md +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/.travis.yml +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/CONTRIBUTING.md +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/GOVERNANCE.md +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/LICENSE +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/README.md +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/doc/wg-meetings/2015-01-30.md +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/duplex-browser.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/duplex.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/_stream_duplex.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/_stream_passthrough.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/_stream_readable.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/_stream_transform.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/_stream_writable.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/internal/streams/BufferList.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/internal/streams/destroy.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/internal/streams/stream-browser.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/internal/streams/stream.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/package.json +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/passthrough.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/readable-browser.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/readable.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/transform.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/writable-browser.js +0 -0
- /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/writable.js +0 -0
- /package/node_modules/{safe-buffer → jszip/node_modules/safe-buffer}/LICENSE +0 -0
- /package/node_modules/{safe-buffer → jszip/node_modules/safe-buffer}/README.md +0 -0
- /package/node_modules/{safe-buffer → jszip/node_modules/safe-buffer}/index.d.ts +0 -0
- /package/node_modules/{safe-buffer → jszip/node_modules/safe-buffer}/index.js +0 -0
- /package/node_modules/{safe-buffer → jszip/node_modules/safe-buffer}/package.json +0 -0
- /package/node_modules/{setimmediate → jszip/node_modules/setimmediate}/LICENSE.txt +0 -0
- /package/node_modules/{setimmediate → jszip/node_modules/setimmediate}/package.json +0 -0
- /package/node_modules/{setimmediate → jszip/node_modules/setimmediate}/setImmediate.js +0 -0
- /package/node_modules/{string_decoder → jszip/node_modules/string_decoder}/.travis.yml +0 -0
- /package/node_modules/{string_decoder → jszip/node_modules/string_decoder}/LICENSE +0 -0
- /package/node_modules/{string_decoder → jszip/node_modules/string_decoder}/README.md +0 -0
- /package/node_modules/{string_decoder → jszip/node_modules/string_decoder}/lib/string_decoder.js +0 -0
- /package/node_modules/{string_decoder → jszip/node_modules/string_decoder}/package.json +0 -0
- /package/node_modules/{util-deprecate → jszip/node_modules/util-deprecate}/History.md +0 -0
- /package/node_modules/{util-deprecate → jszip/node_modules/util-deprecate}/LICENSE +0 -0
- /package/node_modules/{util-deprecate → jszip/node_modules/util-deprecate}/README.md +0 -0
- /package/node_modules/{util-deprecate → jszip/node_modules/util-deprecate}/browser.js +0 -0
- /package/node_modules/{util-deprecate → jszip/node_modules/util-deprecate}/node.js +0 -0
- /package/node_modules/{util-deprecate → jszip/node_modules/util-deprecate}/package.json +0 -0
- /package/node_modules/{parse-ms → pretty-ms/node_modules/parse-ms}/index.d.ts +0 -0
- /package/node_modules/{parse-ms → pretty-ms/node_modules/parse-ms}/index.js +0 -0
- /package/node_modules/{parse-ms → pretty-ms/node_modules/parse-ms}/license +0 -0
- /package/node_modules/{parse-ms → pretty-ms/node_modules/parse-ms}/package.json +0 -0
- /package/node_modules/{parse-ms → pretty-ms/node_modules/parse-ms}/readme.md +0 -0
package/dist/index.js
CHANGED
|
@@ -1,2327 +1,16 @@
|
|
|
1
|
-
import { a as buildGoogleMeetCalendarDayWindow, c as normalizeMeetUrl, i as resolveGoogleMeetGatewayOperationTimeoutMs, n as DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND, o as findGoogleMeetCalendarEvent, r as resolveGoogleMeetConfig, s as listGoogleMeetCalendarEvents, t as DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND } from "./config-BcaS1BDr.js";
|
|
2
1
|
import { t as GOOGLE_MEET_CLI_DESCRIPTOR } from "./cli-output-mode-BwH_6tH8.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { n as GOOGLE_MEET_NODE_COMMAND } from "./google-meet-platform-constants-Bs5iAg3E.js";
|
|
3
|
+
import { r as resolveGoogleMeetConfig } from "./config-wBK8loBb.js";
|
|
4
|
+
import { a as createLazyGoogleMeetNodeInvokePolicy, c as loadGoogleMeetCliModule, d as normalizeMode, f as normalizeTransport, g as testing, h as shouldJoinCreatedMeet, i as createGoogleMeetRuntimeAccessor, l as loadGoogleMeetNodeHostModule, m as sendGoogleMeetGatewayError, n as assertGoogleMeetAgentToolActionSupported, o as formatGoogleMeetGatewayError, p as resolveMeetingInput, r as callGoogleMeetGatewayFromTool, s as keepTrustedToolAgentId, t as asParamRecord, u as loadGoogleMeetPluginHelpers } from "./plugin-registration-rSOssQmm.js";
|
|
6
5
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
7
|
-
import {
|
|
8
|
-
import { asRecord, normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
|
+
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
9
7
|
import { jsonResult } from "openclaw/plugin-sdk/tool-results";
|
|
10
|
-
import {
|
|
11
|
-
import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
12
|
-
import { spawnSync } from "node:child_process";
|
|
13
|
-
import { MeetingPlatformAdapter, MeetingSessionRuntime, addMeetingSetupCheck, createLocalMeetingRealtimeAudioTransport, createMeetingBrowserNodeInvokePolicy, createMeetingNodeHost, createMeetingRealtimeEngineBindings, createMeetingSession, createMeetingSetupStatus, createMeetingVoiceCallGateway, createNodeMeetingRealtimeAudioTransport, endMeetingVoiceCallGatewayCall, getMeetingVoiceCallGatewayCall, isMeetingVoiceCallMissingError, joinMeetingViaVoiceCallGateway, leaveMeetingWithBrowser, openMeetingWithBrowser, readMeetingTranscriptWithBrowser, recoverMeetingBrowserTab, resolveLocalMeetingBrowserRequest, speakMeetingViaVoiceCallGateway, startMeetingAgentRealtimeEngine, startMeetingRealtimeEngine } from "openclaw/plugin-sdk/meeting-runtime";
|
|
14
|
-
import { addTimerTimeoutGraceMs } from "openclaw/plugin-sdk/number-runtime";
|
|
15
|
-
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
16
|
-
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
|
17
|
-
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
|
|
18
|
-
import fs from "node:fs";
|
|
19
|
-
import os from "node:os";
|
|
20
|
-
import path from "node:path";
|
|
8
|
+
import { optionalPositiveIntegerSchema } from "openclaw/plugin-sdk/channel-actions";
|
|
21
9
|
import { Type } from "typebox";
|
|
22
|
-
//#region extensions/google-meet/src/transports/chrome-audio-device.ts
|
|
23
|
-
const GOOGLE_MEET_SYSTEM_PROFILER_COMMAND = "/usr/sbin/system_profiler";
|
|
24
|
-
function outputMentionsBlackHole2ch(output) {
|
|
25
|
-
return /\bBlackHole\s+2ch\b/i.test(output);
|
|
26
|
-
}
|
|
27
|
-
//#endregion
|
|
28
|
-
//#region extensions/google-meet/src/transports/types.ts
|
|
29
|
-
const GOOGLE_MEET_TRANSCRIPT_MAX_LINES = 2e3;
|
|
30
|
-
//#endregion
|
|
31
|
-
//#region extensions/google-meet/src/transports/google-meet-page-scripts.ts
|
|
32
|
-
const GOOGLE_MEET_CAPTION_SETTLE_MS = 1e3;
|
|
33
|
-
function meetStatusScript(params) {
|
|
34
|
-
return `async () => {
|
|
35
|
-
const text = (node) => (node?.innerText || node?.textContent || "").trim();
|
|
36
|
-
const manualActionFor = (reason, message) => ({ reason, message });
|
|
37
|
-
const allowMicrophone = ${JSON.stringify(params.allowMicrophone)};
|
|
38
|
-
const captionSessionId = ${JSON.stringify(params.captionSessionId)};
|
|
39
|
-
const captureCaptions = ${JSON.stringify(params.captureCaptions)};
|
|
40
|
-
const readOnly = ${JSON.stringify(Boolean(params.readOnly))};
|
|
41
|
-
const buttons = [...document.querySelectorAll('button')];
|
|
42
|
-
const buttonLabel = (button) =>
|
|
43
|
-
[
|
|
44
|
-
button.getAttribute("aria-label"),
|
|
45
|
-
button.getAttribute("data-tooltip"),
|
|
46
|
-
text(button),
|
|
47
|
-
]
|
|
48
|
-
.filter(Boolean)
|
|
49
|
-
.join(" ");
|
|
50
|
-
const buttonLabels = buttons.map(buttonLabel).filter(Boolean);
|
|
51
|
-
const notes = [];
|
|
52
|
-
let audioOutputRouted;
|
|
53
|
-
let audioOutputDeviceLabel;
|
|
54
|
-
let audioOutputRouteError;
|
|
55
|
-
const findButton = (pattern) =>
|
|
56
|
-
buttons.find((button) => {
|
|
57
|
-
const label = buttonLabel(button);
|
|
58
|
-
return pattern.test(label) && !button.disabled;
|
|
59
|
-
});
|
|
60
|
-
const findCallControlButton = (pattern) =>
|
|
61
|
-
buttons.find((button) => {
|
|
62
|
-
const label = buttonLabel(button);
|
|
63
|
-
return pattern.test(label) && !/remotely mute|someone else/i.test(label) && !button.disabled;
|
|
64
|
-
});
|
|
65
|
-
const input = [...document.querySelectorAll('input')].find((el) =>
|
|
66
|
-
/your name/i.test(el.getAttribute('aria-label') || el.placeholder || '')
|
|
67
|
-
);
|
|
68
|
-
if (!readOnly && ${JSON.stringify(params.autoJoin)} && input && !input.value) {
|
|
69
|
-
input.focus();
|
|
70
|
-
input.value = ${JSON.stringify(params.guestName)};
|
|
71
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
72
|
-
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
73
|
-
}
|
|
74
|
-
const pageText = text(document.body).toLowerCase();
|
|
75
|
-
const permissionText = [pageText, ...buttonLabels].join("\\n");
|
|
76
|
-
const host = location.hostname.toLowerCase();
|
|
77
|
-
const pageUrl = location.href;
|
|
78
|
-
const permissionNeeded = /permission needed|microphone problem|speaker problem|allow.*(microphone|camera)|blocked.*(microphone|camera)|permission.*(microphone|camera|speaker)/i.test(permissionText);
|
|
79
|
-
let mic = findCallControlButton(/^\\s*turn (?:off|on) microphone\\b/i);
|
|
80
|
-
if (!mic) {
|
|
81
|
-
const callControls = document.querySelector('[role="region"][aria-label="Call controls"]');
|
|
82
|
-
mic = [...(callControls?.querySelectorAll('button') || [])].find((button) =>
|
|
83
|
-
/^\\s*turn (?:off|on) microphone\\b/i.test(buttonLabel(button))
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
if (!readOnly && allowMicrophone && mic && /turn on microphone/i.test(buttonLabel(mic))) {
|
|
87
|
-
mic.click();
|
|
88
|
-
notes.push("Attempted to turn on the Meet microphone for talk-back mode.");
|
|
89
|
-
}
|
|
90
|
-
if (!readOnly && !allowMicrophone && mic && /turn off microphone/i.test(mic.getAttribute('aria-label') || text(mic))) {
|
|
91
|
-
mic.click();
|
|
92
|
-
notes.push("Muted Meet microphone for observe-only mode.");
|
|
93
|
-
}
|
|
94
|
-
const joinElsewhere = findButton(/join here too/i);
|
|
95
|
-
const join = !readOnly && ${JSON.stringify(params.autoJoin)}
|
|
96
|
-
? findButton(/join now|ask to join/i)
|
|
97
|
-
: null;
|
|
98
|
-
if (join) join.click();
|
|
99
|
-
const microphoneChoice = findButton(/\\buse microphone\\b/i);
|
|
100
|
-
const noMicrophoneChoice = findButton(/\\b(continue|join|use) without (microphone|mic)\\b|\\bnot now\\b/i);
|
|
101
|
-
if (!readOnly && allowMicrophone && microphoneChoice) {
|
|
102
|
-
microphoneChoice.click();
|
|
103
|
-
notes.push("Accepted Meet microphone prompt with browser automation.");
|
|
104
|
-
} else if (!readOnly && !allowMicrophone && noMicrophoneChoice) {
|
|
105
|
-
noMicrophoneChoice.click();
|
|
106
|
-
notes.push("Skipped Meet microphone prompt for observe-only mode.");
|
|
107
|
-
}
|
|
108
|
-
const inCall = buttons.some((button) => /leave call/i.test(button.getAttribute('aria-label') || text(button)));
|
|
109
|
-
const routeMeetAudioOutput = async () => {
|
|
110
|
-
if (
|
|
111
|
-
!allowMicrophone ||
|
|
112
|
-
typeof navigator === 'undefined' ||
|
|
113
|
-
!navigator.mediaDevices?.enumerateDevices
|
|
114
|
-
) return;
|
|
115
|
-
const mediaElements = [...document.querySelectorAll('audio, video')]
|
|
116
|
-
.filter((el) => typeof el.setSinkId === 'function');
|
|
117
|
-
if (mediaElements.length === 0) return;
|
|
118
|
-
try {
|
|
119
|
-
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
120
|
-
const output = devices.find((device) =>
|
|
121
|
-
device.kind === 'audiooutput' && /\\bBlackHole\\s+2ch\\b/i.test(device.label || '')
|
|
122
|
-
) || devices.find((device) =>
|
|
123
|
-
device.kind === 'audiooutput' && /\\bBlackHole\\b/i.test(device.label || '')
|
|
124
|
-
);
|
|
125
|
-
if (!output?.deviceId) {
|
|
126
|
-
if (devices.some((device) => device.kind === 'audiooutput')) {
|
|
127
|
-
notes.push("BlackHole 2ch speaker output was not visible to Meet.");
|
|
128
|
-
}
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
let routed = 0;
|
|
132
|
-
for (const element of mediaElements) {
|
|
133
|
-
if (element.sinkId !== output.deviceId) {
|
|
134
|
-
if (readOnly) {
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
await element.setSinkId(output.deviceId);
|
|
138
|
-
routed += 1;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
audioOutputRouted = mediaElements.some((element) => element.sinkId === output.deviceId);
|
|
142
|
-
audioOutputDeviceLabel = output.label || "BlackHole 2ch";
|
|
143
|
-
if (!readOnly && audioOutputRouted) {
|
|
144
|
-
notes.push(
|
|
145
|
-
routed > 0
|
|
146
|
-
? \`Routed Meet media output to \${audioOutputDeviceLabel}.\`
|
|
147
|
-
: \`Meet media output already routed to \${audioOutputDeviceLabel}.\`
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
} catch (error) {
|
|
151
|
-
audioOutputRouteError = error?.message || String(error);
|
|
152
|
-
notes.push(\`Could not route Meet speaker output to BlackHole 2ch: \${audioOutputRouteError}\`);
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
if (inCall) {
|
|
156
|
-
await routeMeetAudioOutput();
|
|
157
|
-
}
|
|
158
|
-
let captioning = false;
|
|
159
|
-
let captionsEnabledAttempted = false;
|
|
160
|
-
let transcriptLines = 0;
|
|
161
|
-
let lastCaptionAt;
|
|
162
|
-
let lastCaptionSpeaker;
|
|
163
|
-
let lastCaptionText;
|
|
164
|
-
let recentTranscript = [];
|
|
165
|
-
const captionSelector = '[role="region"][aria-label*="aption" i], [aria-live="polite"][role="region"], div[aria-live="polite"]';
|
|
166
|
-
const captionState = (() => {
|
|
167
|
-
if (!captureCaptions) return undefined;
|
|
168
|
-
const w = window;
|
|
169
|
-
if (!inCall && !w.__openclawMeetCaptions) return undefined;
|
|
170
|
-
// A reused tab starts a fresh logical transcript for each OpenClaw session.
|
|
171
|
-
// Status refreshes omit the id, so they preserve the active page-owned buffer.
|
|
172
|
-
if (!w.__openclawMeetCaptions || (captionSessionId && w.__openclawMeetCaptions.sessionId !== captionSessionId)) {
|
|
173
|
-
if (w.__openclawMeetCaptions?.settleTimer !== undefined) {
|
|
174
|
-
clearTimeout(w.__openclawMeetCaptions.settleTimer);
|
|
175
|
-
}
|
|
176
|
-
w.__openclawMeetCaptions?.observer?.disconnect?.();
|
|
177
|
-
w.__openclawMeetCaptions = {
|
|
178
|
-
sessionId: captionSessionId,
|
|
179
|
-
// Epochs cross document lifetimes in the runtime transcript cursor.
|
|
180
|
-
// Strong UUIDs keep a reloaded page distinct from its prior buffer.
|
|
181
|
-
epoch: crypto.randomUUID(),
|
|
182
|
-
enabledAttempted: false,
|
|
183
|
-
observerInstalled: false,
|
|
184
|
-
observer: undefined,
|
|
185
|
-
droppedLines: 0,
|
|
186
|
-
lines: [],
|
|
187
|
-
settleTimer: undefined,
|
|
188
|
-
visible: []
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
return w.__openclawMeetCaptions;
|
|
192
|
-
})();
|
|
193
|
-
const normalizeCaption = (speaker, captionText) => {
|
|
194
|
-
if (!captionState) return;
|
|
195
|
-
const clean = String(captionText || "").replace(/\\s+/g, " ").trim();
|
|
196
|
-
const cleanSpeaker = String(speaker || "").replace(/\\s+/g, " ").trim();
|
|
197
|
-
if (!clean || clean.length < 2) return undefined;
|
|
198
|
-
if (/^(turn on captions|turn off captions|captions)$/i.test(clean)) return undefined;
|
|
199
|
-
return { speaker: cleanSpeaker || undefined, text: clean };
|
|
200
|
-
};
|
|
201
|
-
const commitLines = (state, entries) => {
|
|
202
|
-
state.lines.push(...entries.map((entry) => ({
|
|
203
|
-
at: entry.at,
|
|
204
|
-
speaker: entry.speaker,
|
|
205
|
-
text: entry.text
|
|
206
|
-
})));
|
|
207
|
-
const excess = state.lines.length - ${GOOGLE_MEET_TRANSCRIPT_MAX_LINES};
|
|
208
|
-
if (excess > 0) {
|
|
209
|
-
state.lines.splice(0, excess);
|
|
210
|
-
state.droppedLines = (state.droppedLines || 0) + excess;
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
const scrapeCaptions = () => {
|
|
214
|
-
if (!captionState) return;
|
|
215
|
-
const regions = [...document.querySelectorAll(captionSelector)];
|
|
216
|
-
const rows = [];
|
|
217
|
-
for (const region of regions) {
|
|
218
|
-
const raw = text(region);
|
|
219
|
-
if (!raw) continue;
|
|
220
|
-
const pieces = raw.split(/\\n+/).map((part) => part.trim()).filter(Boolean);
|
|
221
|
-
const row = pieces.length >= 2
|
|
222
|
-
? normalizeCaption(pieces[0], pieces.slice(1).join(" "))
|
|
223
|
-
: normalizeCaption("", pieces[0] || raw);
|
|
224
|
-
if (row) rows.push({ ...row, node: region });
|
|
225
|
-
}
|
|
226
|
-
if (rows.length === 0) {
|
|
227
|
-
// Meet briefly removes caption rows while rerendering. Keep them mutable
|
|
228
|
-
// for one settle window so a DOM gap cannot fabricate a repeated line.
|
|
229
|
-
if (captionState.visible.length > 0 && captionState.settleTimer === undefined) {
|
|
230
|
-
const pendingState = captionState;
|
|
231
|
-
pendingState.settleTimer = setTimeout(() => {
|
|
232
|
-
if (window.__openclawMeetCaptions !== pendingState) return;
|
|
233
|
-
commitLines(pendingState, pendingState.visible);
|
|
234
|
-
pendingState.visible = [];
|
|
235
|
-
pendingState.settleTimer = undefined;
|
|
236
|
-
}, ${GOOGLE_MEET_CAPTION_SETTLE_MS});
|
|
237
|
-
}
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
if (captionState.settleTimer !== undefined) {
|
|
241
|
-
clearTimeout(captionState.settleTimer);
|
|
242
|
-
captionState.settleTimer = undefined;
|
|
243
|
-
}
|
|
244
|
-
const previous = Array.isArray(captionState.visible) ? captionState.visible : [];
|
|
245
|
-
const unmatchedPrevious = [...previous];
|
|
246
|
-
const nextVisible = [];
|
|
247
|
-
const now = Date.now();
|
|
248
|
-
for (let index = 0; index < rows.length; index += 1) {
|
|
249
|
-
const row = rows[index];
|
|
250
|
-
const priorIndex = unmatchedPrevious.findIndex((candidate) => {
|
|
251
|
-
const sameTextLifecycle =
|
|
252
|
-
candidate.text === row.text ||
|
|
253
|
-
row.text.startsWith(candidate.text) ||
|
|
254
|
-
candidate.text.startsWith(row.text);
|
|
255
|
-
const sameDomLifecycle =
|
|
256
|
-
candidate.node === row.node || now - candidate.seenAt <= ${GOOGLE_MEET_CAPTION_SETTLE_MS};
|
|
257
|
-
return candidate.speaker === row.speaker && sameTextLifecycle && sameDomLifecycle;
|
|
258
|
-
});
|
|
259
|
-
const prior = priorIndex >= 0 ? unmatchedPrevious.splice(priorIndex, 1)[0] : undefined;
|
|
260
|
-
const sameSpeaker = Boolean(prior) && prior.speaker === row.speaker;
|
|
261
|
-
if (sameSpeaker && prior.text === row.text) {
|
|
262
|
-
prior.node = row.node;
|
|
263
|
-
prior.seenAt = now;
|
|
264
|
-
nextVisible.push(prior);
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
if (sameSpeaker && row.text.startsWith(prior.text)) {
|
|
268
|
-
prior.text = row.text;
|
|
269
|
-
prior.node = row.node;
|
|
270
|
-
prior.seenAt = now;
|
|
271
|
-
nextVisible.push(prior);
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
if (sameSpeaker && prior.text.startsWith(row.text)) {
|
|
275
|
-
prior.node = row.node;
|
|
276
|
-
prior.seenAt = now;
|
|
277
|
-
nextVisible.push(prior);
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
const entry = {
|
|
281
|
-
at: new Date().toISOString(),
|
|
282
|
-
node: row.node,
|
|
283
|
-
seenAt: now,
|
|
284
|
-
speaker: row.speaker,
|
|
285
|
-
text: row.text
|
|
286
|
-
};
|
|
287
|
-
nextVisible.push(entry);
|
|
288
|
-
}
|
|
289
|
-
commitLines(captionState, unmatchedPrevious);
|
|
290
|
-
captionState.visible = nextVisible;
|
|
291
|
-
};
|
|
292
|
-
if (captionState) {
|
|
293
|
-
if (!readOnly && inCall && !captionState.enabledAttempted) {
|
|
294
|
-
const captionButton = findButton(/turn on captions|show captions|captions/i);
|
|
295
|
-
const captionLabel = captionButton ? (captionButton.getAttribute("aria-label") || captionButton.getAttribute("data-tooltip") || text(captionButton)) : "";
|
|
296
|
-
if (captionButton) {
|
|
297
|
-
captionState.enabledAttempted = true;
|
|
298
|
-
captionsEnabledAttempted = true;
|
|
299
|
-
if (!/turn off captions|hide captions/i.test(captionLabel)) {
|
|
300
|
-
captionButton.click();
|
|
301
|
-
notes.push("Attempted to enable Meet captions for observe-only transcript health.");
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
} else if (captionState.enabledAttempted) {
|
|
305
|
-
captionsEnabledAttempted = true;
|
|
306
|
-
}
|
|
307
|
-
if (inCall && !captionState.observerInstalled) {
|
|
308
|
-
captionState.observerInstalled = true;
|
|
309
|
-
captionState.observer = new MutationObserver(scrapeCaptions);
|
|
310
|
-
captionState.observer.observe(document.body, {
|
|
311
|
-
childList: true,
|
|
312
|
-
subtree: true,
|
|
313
|
-
characterData: true
|
|
314
|
-
});
|
|
315
|
-
notes.push("Installed Meet caption observer for observe-only transcript health.");
|
|
316
|
-
}
|
|
317
|
-
if (inCall) {
|
|
318
|
-
scrapeCaptions();
|
|
319
|
-
}
|
|
320
|
-
const committedLines = Array.isArray(captionState.lines) ? captionState.lines : [];
|
|
321
|
-
const visibleLines = Array.isArray(captionState.visible) ? captionState.visible : [];
|
|
322
|
-
const lines = [...committedLines, ...visibleLines];
|
|
323
|
-
const last = lines[lines.length - 1];
|
|
324
|
-
captioning = document.querySelector(captionSelector) !== null || lines.length > 0;
|
|
325
|
-
transcriptLines = (captionState.droppedLines || 0) + lines.length;
|
|
326
|
-
lastCaptionAt = last?.at;
|
|
327
|
-
lastCaptionSpeaker = last?.speaker;
|
|
328
|
-
lastCaptionText = last?.text;
|
|
329
|
-
recentTranscript = lines.slice(-5);
|
|
330
|
-
}
|
|
331
|
-
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);
|
|
332
|
-
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)
|
|
333
|
-
? pageText.match(/you left the meeting|you.?ve left the meeting|removed from the meeting|you were removed|call ended|meeting ended/i)?.[0]
|
|
334
|
-
: undefined;
|
|
335
|
-
let manualAction;
|
|
336
|
-
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))) {
|
|
337
|
-
manualAction = manualActionFor("google-login-required", "Sign in to Google in the OpenClaw browser profile, then retry the Meet join.");
|
|
338
|
-
} else if (!inCall && joinElsewhere) {
|
|
339
|
-
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.");
|
|
340
|
-
} 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)) {
|
|
341
|
-
manualAction = manualActionFor("meet-admission-required", "Admit the OpenClaw browser participant in Google Meet, then retry speech.");
|
|
342
|
-
} else if (permissionNeeded) {
|
|
343
|
-
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.");
|
|
344
|
-
} else if (!inCall && (allowMicrophone ? !microphoneChoice : !noMicrophoneChoice) && /do you want people to hear you in the meeting/i.test(pageText)) {
|
|
345
|
-
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.");
|
|
346
|
-
}
|
|
347
|
-
return JSON.stringify({
|
|
348
|
-
clickedJoin: Boolean(join),
|
|
349
|
-
clickedMicrophoneChoice: Boolean(allowMicrophone && microphoneChoice),
|
|
350
|
-
inCall,
|
|
351
|
-
micMuted: mic ? /turn on microphone/i.test(buttonLabel(mic)) : undefined,
|
|
352
|
-
lobbyWaiting,
|
|
353
|
-
leaveReason,
|
|
354
|
-
captioning,
|
|
355
|
-
captionsEnabledAttempted,
|
|
356
|
-
transcriptLines,
|
|
357
|
-
lastCaptionAt,
|
|
358
|
-
lastCaptionSpeaker,
|
|
359
|
-
lastCaptionText,
|
|
360
|
-
recentTranscript,
|
|
361
|
-
audioOutputRouted,
|
|
362
|
-
audioOutputDeviceLabel,
|
|
363
|
-
audioOutputRouteError,
|
|
364
|
-
manualAction,
|
|
365
|
-
title: document.title,
|
|
366
|
-
url: pageUrl,
|
|
367
|
-
notes
|
|
368
|
-
});
|
|
369
|
-
}`;
|
|
370
|
-
}
|
|
371
|
-
function meetTranscriptScript(meetingUrl, meetingSessionId, finalize) {
|
|
372
|
-
const expectedMeetingUrl = normalizeMeetUrlForReuse(meetingUrl);
|
|
373
|
-
return `() => {
|
|
374
|
-
const expectedMeetingUrl = ${JSON.stringify(expectedMeetingUrl)};
|
|
375
|
-
const expectedSessionId = ${JSON.stringify(meetingSessionId)};
|
|
376
|
-
let currentMeetingUrl;
|
|
377
|
-
try {
|
|
378
|
-
const currentUrl = new URL(location.href);
|
|
379
|
-
currentMeetingUrl = currentUrl.origin + currentUrl.pathname.toLowerCase().replace(/\\/$/, "");
|
|
380
|
-
} catch {
|
|
381
|
-
return JSON.stringify({ urlMatched: false });
|
|
382
|
-
}
|
|
383
|
-
if (!expectedMeetingUrl || currentMeetingUrl !== expectedMeetingUrl) {
|
|
384
|
-
return JSON.stringify({ urlMatched: false });
|
|
385
|
-
}
|
|
386
|
-
const state = window.__openclawMeetCaptions;
|
|
387
|
-
if (state?.sessionId && state.sessionId !== expectedSessionId) {
|
|
388
|
-
return JSON.stringify({ urlMatched: true, sessionMatched: false });
|
|
389
|
-
}
|
|
390
|
-
if (${JSON.stringify(finalize)} && Array.isArray(state?.visible) && state.visible.length > 0) {
|
|
391
|
-
if (state.settleTimer !== undefined) clearTimeout(state.settleTimer);
|
|
392
|
-
state.settleTimer = undefined;
|
|
393
|
-
state.lines = Array.isArray(state.lines) ? state.lines : [];
|
|
394
|
-
state.lines.push(...state.visible.map((entry) => ({
|
|
395
|
-
at: entry.at,
|
|
396
|
-
speaker: entry.speaker,
|
|
397
|
-
text: entry.text
|
|
398
|
-
})));
|
|
399
|
-
state.visible = [];
|
|
400
|
-
const excess = state.lines.length - ${GOOGLE_MEET_TRANSCRIPT_MAX_LINES};
|
|
401
|
-
if (excess > 0) {
|
|
402
|
-
state.lines.splice(0, excess);
|
|
403
|
-
state.droppedLines = (state.droppedLines || 0) + excess;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
const lines = Array.isArray(state?.lines) ? state.lines : [];
|
|
407
|
-
return JSON.stringify({
|
|
408
|
-
urlMatched: true,
|
|
409
|
-
sessionMatched: true,
|
|
410
|
-
epoch: typeof state?.epoch === "string" ? state.epoch : undefined,
|
|
411
|
-
droppedLines: Number.isFinite(state?.droppedLines) ? Math.max(0, Math.trunc(state.droppedLines)) : 0,
|
|
412
|
-
lines: lines.map((line) => ({
|
|
413
|
-
at: typeof line?.at === "string" ? line.at : undefined,
|
|
414
|
-
speaker: typeof line?.speaker === "string" ? line.speaker : undefined,
|
|
415
|
-
text: typeof line?.text === "string" ? line.text : ""
|
|
416
|
-
})).filter((line) => line.text)
|
|
417
|
-
});
|
|
418
|
-
}`;
|
|
419
|
-
}
|
|
420
|
-
function meetLeaveScript(meetingUrl) {
|
|
421
|
-
const expectedMeetingUrl = normalizeMeetUrlForReuse(meetingUrl);
|
|
422
|
-
return `() => {
|
|
423
|
-
const expectedMeetingUrl = ${JSON.stringify(expectedMeetingUrl)};
|
|
424
|
-
let currentMeetingUrl;
|
|
425
|
-
try {
|
|
426
|
-
const currentUrl = new URL(location.href);
|
|
427
|
-
currentMeetingUrl = currentUrl.origin + currentUrl.pathname.toLowerCase().replace(/\\/$/, "");
|
|
428
|
-
} catch {
|
|
429
|
-
return JSON.stringify({ departed: false });
|
|
430
|
-
}
|
|
431
|
-
if (!expectedMeetingUrl) {
|
|
432
|
-
return JSON.stringify({ departed: false });
|
|
433
|
-
}
|
|
434
|
-
if (currentMeetingUrl !== expectedMeetingUrl) {
|
|
435
|
-
return JSON.stringify({ departed: true, urlMatched: false });
|
|
436
|
-
}
|
|
437
|
-
const text = (node) => (node?.innerText || node?.textContent || "").trim();
|
|
438
|
-
// Locale-independent fallback: Meet renders the leave control as a Material
|
|
439
|
-
// Symbols icon whose ligature text is "call_end" in every UI language, so a
|
|
440
|
-
// localized aria-label (e.g. "Anruf verlassen") still resolves to the button.
|
|
441
|
-
const hasLeaveIcon = (button) => {
|
|
442
|
-
const icon = button.querySelector ? button.querySelector("i") : null;
|
|
443
|
-
return icon ? (icon.textContent || "").trim() === "call_end" : false;
|
|
444
|
-
};
|
|
445
|
-
const buttons = [...document.querySelectorAll('button')];
|
|
446
|
-
const label = (button) => [
|
|
447
|
-
button.getAttribute("aria-label"),
|
|
448
|
-
button.getAttribute("data-tooltip"),
|
|
449
|
-
text(button),
|
|
450
|
-
]
|
|
451
|
-
.filter(Boolean)
|
|
452
|
-
.join(" ");
|
|
453
|
-
const postCall = buttons.some((button) => /\\b(rejoin|return to home screen)\\b/i.test(label(button)));
|
|
454
|
-
if (postCall) {
|
|
455
|
-
return JSON.stringify({ departed: true, urlMatched: true });
|
|
456
|
-
}
|
|
457
|
-
// Managed join tabs are reused only after the English-tab gate or opened
|
|
458
|
-
// through the English-UI helper, so follow-up labels are pinned to English.
|
|
459
|
-
const confirmation = buttons.find((button) => {
|
|
460
|
-
return !button.disabled && /\\bleave meeting\\b/i.test(label(button));
|
|
461
|
-
});
|
|
462
|
-
if (confirmation) {
|
|
463
|
-
confirmation.click();
|
|
464
|
-
return JSON.stringify({ departed: false, leaveAction: "confirm", urlMatched: true });
|
|
465
|
-
}
|
|
466
|
-
const leave = buttons.find((button) => {
|
|
467
|
-
if (button.disabled) return false;
|
|
468
|
-
return /leave call/i.test(label(button)) || hasLeaveIcon(button);
|
|
469
|
-
});
|
|
470
|
-
if (leave) {
|
|
471
|
-
leave.click();
|
|
472
|
-
return JSON.stringify({ departed: false, leaveAction: "leave", urlMatched: true });
|
|
473
|
-
}
|
|
474
|
-
return JSON.stringify({ departed: false, urlMatched: true });
|
|
475
|
-
}`;
|
|
476
|
-
}
|
|
477
|
-
//#endregion
|
|
478
|
-
//#region extensions/google-meet/src/transports/twilio.ts
|
|
479
|
-
const DTMF_PATTERN = /^[0-9*#wWpP,]+$/;
|
|
480
|
-
function normalizeDialInNumber(value) {
|
|
481
|
-
const normalized = normalizeOptionalString(value);
|
|
482
|
-
if (!normalized) return;
|
|
483
|
-
const compact = normalized.replace(/[()\s.-]/g, "");
|
|
484
|
-
if (!/^\+?[0-9]{5,20}$/.test(compact)) throw new Error("dialInNumber must be a phone number");
|
|
485
|
-
return compact;
|
|
486
|
-
}
|
|
487
|
-
function normalizeDtmfSequence(value) {
|
|
488
|
-
const normalized = normalizeOptionalString(value);
|
|
489
|
-
if (!normalized) return;
|
|
490
|
-
const compact = normalized.replace(/\s+/g, "");
|
|
491
|
-
if (!DTMF_PATTERN.test(compact)) throw new Error("dtmfSequence may only contain digits, *, #, comma, w, p");
|
|
492
|
-
return compact;
|
|
493
|
-
}
|
|
494
|
-
function buildMeetDtmfSequence(params) {
|
|
495
|
-
const explicit = normalizeDtmfSequence(params.dtmfSequence);
|
|
496
|
-
if (explicit) return explicit;
|
|
497
|
-
const pin = normalizeOptionalString(params.pin);
|
|
498
|
-
if (!pin) return;
|
|
499
|
-
const compactPin = pin.replace(/\s+/g, "");
|
|
500
|
-
if (!/^[0-9]+#?$/.test(compactPin)) throw new Error("pin may only contain digits and an optional trailing #");
|
|
501
|
-
return compactPin.endsWith("#") ? compactPin : `${compactPin}#`;
|
|
502
|
-
}
|
|
503
|
-
function prefixDtmfWait(sequence, delayMs) {
|
|
504
|
-
if (!sequence || delayMs <= 0) return sequence;
|
|
505
|
-
const waitCount = Math.ceil(delayMs / 500);
|
|
506
|
-
if (waitCount <= 0) return sequence;
|
|
507
|
-
return `${"w".repeat(waitCount)}${sequence}`;
|
|
508
|
-
}
|
|
509
|
-
//#endregion
|
|
510
|
-
//#region extensions/google-meet/src/transports/google-meet-platform-adapter.ts
|
|
511
|
-
function parsePermissionGrantNotes(result) {
|
|
512
|
-
const record = result && typeof result === "object" ? result : {};
|
|
513
|
-
const unsupportedPermissions = Array.isArray(record.unsupportedPermissions) ? record.unsupportedPermissions.filter((value) => typeof value === "string") : [];
|
|
514
|
-
const notes = ["Granted Meet microphone/camera permissions through browser control."];
|
|
515
|
-
if (unsupportedPermissions.includes("speakerSelection")) notes.push("Chrome did not accept the optional Meet speaker-selection permission.");
|
|
516
|
-
return notes;
|
|
517
|
-
}
|
|
518
|
-
const manualActionCategories = /* @__PURE__ */ new Map([
|
|
519
|
-
["browser-control-unavailable", "browser-control-unavailable"],
|
|
520
|
-
["google-login-required", "login-required"],
|
|
521
|
-
["meet-admission-required", "admission-required"],
|
|
522
|
-
["meet-audio-choice-required", "audio-choice-required"],
|
|
523
|
-
["meet-locale-required", "locale-required"],
|
|
524
|
-
["meet-permission-required", "permission-required"],
|
|
525
|
-
["meet-session-conflict", "session-conflict"]
|
|
526
|
-
]);
|
|
527
|
-
function classifyManualActionReason(reason) {
|
|
528
|
-
return manualActionCategories.get(reason) ?? "custom";
|
|
529
|
-
}
|
|
530
|
-
const GOOGLE_MEET_PLATFORM_ADAPTER = MeetingPlatformAdapter.create({
|
|
531
|
-
id: "google-meet",
|
|
532
|
-
displayName: "Google Meet",
|
|
533
|
-
browserLabel: "Meet",
|
|
534
|
-
logScope: "[google-meet]",
|
|
535
|
-
agentConsult: {
|
|
536
|
-
surface: "a private Google Meet",
|
|
537
|
-
userLabel: "Participant",
|
|
538
|
-
assistantLabel: "Agent",
|
|
539
|
-
questionSourceLabel: "participant",
|
|
540
|
-
workingResponseLabel: "participant",
|
|
541
|
-
extraSystemPrompt: [
|
|
542
|
-
"You are a behind-the-scenes consultant for a live meeting voice agent.",
|
|
543
|
-
"Prioritize a fast, speakable answer over exhaustive investigation.",
|
|
544
|
-
"For tool-backed status checks, prefer one or two bounded read-only queries before answering.",
|
|
545
|
-
"Do not print secret values or dump environment variables; only check whether required configuration is present.",
|
|
546
|
-
"Be accurate, brief, and speakable."
|
|
547
|
-
].join(" ")
|
|
548
|
-
},
|
|
549
|
-
session: {
|
|
550
|
-
idPrefix: "meet",
|
|
551
|
-
participantIdentity: (transport) => transport === "twilio" ? "Twilio phone participant" : transport === "chrome-node" ? "signed-in Google Chrome profile on a paired node" : "signed-in Google Chrome profile"
|
|
552
|
-
},
|
|
553
|
-
nodeCommandName: GOOGLE_MEET_NODE_COMMAND,
|
|
554
|
-
nodeConfigPath: "plugins.entries.google-meet.config.chromeNode.node",
|
|
555
|
-
urls: {
|
|
556
|
-
validateAndNormalize: normalizeMeetUrl,
|
|
557
|
-
normalizeForReuse: normalizeMeetUrlForReuse,
|
|
558
|
-
isSameMeeting: isSameMeetUrlForReuse,
|
|
559
|
-
buildJoinUrl: (session) => forceMeetEnglishUi(session.url),
|
|
560
|
-
accountHint: readMeetAuthUser,
|
|
561
|
-
isPreferredJoinUrl: isEnglishMeetTab,
|
|
562
|
-
isRecoverableTab: isRecoverableMeetTab,
|
|
563
|
-
localeAction: (tab) => {
|
|
564
|
-
if (!normalizeMeetUrlForReuse(tab.url) || isEnglishMeetTab(tab.url)) return;
|
|
565
|
-
return {
|
|
566
|
-
category: "locale-required",
|
|
567
|
-
reason: "meet-locale-required",
|
|
568
|
-
message: "The existing Meet tab is not pinned to English. Open the meeting with ?hl=en, then retry recovery."
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
},
|
|
572
|
-
browser: {
|
|
573
|
-
allowsMicrophone: MeetingPlatformAdapter.isTalkBackMode,
|
|
574
|
-
buildStatusJoinScript: (params) => meetStatusScript({
|
|
575
|
-
allowMicrophone: MeetingPlatformAdapter.isTalkBackMode(params.mode),
|
|
576
|
-
autoJoin: params.autoJoin,
|
|
577
|
-
captionSessionId: params.meetingSessionId || void 0,
|
|
578
|
-
captureCaptions: params.captureCaptions,
|
|
579
|
-
guestName: params.guestName,
|
|
580
|
-
readOnly: params.readOnly
|
|
581
|
-
}),
|
|
582
|
-
browserControlUnavailable: () => ({
|
|
583
|
-
category: "browser-control-unavailable",
|
|
584
|
-
reason: "browser-control-unavailable",
|
|
585
|
-
message: "Open the OpenClaw browser profile, finish Google Meet login, admission, or permission prompts, then retry."
|
|
586
|
-
}),
|
|
587
|
-
buildLeaveScript: meetLeaveScript,
|
|
588
|
-
captions: {
|
|
589
|
-
enabled: (_mode) => true,
|
|
590
|
-
buildTranscriptScript: ({ finalize, meetingSessionId, meetingUrl }) => meetTranscriptScript(meetingUrl, meetingSessionId, finalize)
|
|
591
|
-
},
|
|
592
|
-
permissions: ({ allowMicrophone }) => allowMicrophone ? {
|
|
593
|
-
origin: "https://meet.google.com",
|
|
594
|
-
permissions: ["audioCapture", "videoCapture"],
|
|
595
|
-
optionalPermissions: ["speakerSelection"]
|
|
596
|
-
} : void 0,
|
|
597
|
-
permissionNotes: ({ allowMicrophone, error, result }) => {
|
|
598
|
-
if (!allowMicrophone) return ["Observe-only mode skips Meet microphone/camera permission grants."];
|
|
599
|
-
if (error) return [`Could not grant Meet media permissions automatically: ${formatErrorMessage(error)}`];
|
|
600
|
-
return parsePermissionGrantNotes(result);
|
|
601
|
-
}
|
|
602
|
-
},
|
|
603
|
-
create: { browser: createMeetWithBrowserProxyOnNode },
|
|
604
|
-
dialIn: { buildPlan: (params) => {
|
|
605
|
-
const number = normalizeDialInNumber(params.dialInNumber ?? params.defaultDialInNumber);
|
|
606
|
-
const pin = params.pin ?? params.defaultPin;
|
|
607
|
-
const rawDtmfSequence = buildMeetDtmfSequence({
|
|
608
|
-
pin,
|
|
609
|
-
dtmfSequence: params.dtmfSequence ?? params.defaultDtmfSequence
|
|
610
|
-
});
|
|
611
|
-
return {
|
|
612
|
-
number,
|
|
613
|
-
pin,
|
|
614
|
-
dtmfSequence: params.dtmfSequence || params.defaultDtmfSequence ? rawDtmfSequence : prefixDtmfWait(rawDtmfSequence, params.dtmfDelayMs)
|
|
615
|
-
};
|
|
616
|
-
} },
|
|
617
|
-
parsing: {
|
|
618
|
-
classifyManualActionReason,
|
|
619
|
-
displayName: "Meet",
|
|
620
|
-
invalidTranscriptMessage: "Google Meet transcript payload is invalid.",
|
|
621
|
-
malformedStatusMessage: "Google Meet browser status JSON is malformed.",
|
|
622
|
-
malformedTranscriptMessage: "Google Meet transcript JSON is malformed.",
|
|
623
|
-
statusFields: (parsed) => ({ leaveReason: typeof parsed.leaveReason === "string" ? parsed.leaveReason : void 0 })
|
|
624
|
-
}
|
|
625
|
-
});
|
|
626
|
-
//#endregion
|
|
627
|
-
//#region extensions/google-meet/src/node-host.ts
|
|
628
|
-
function assertBlackHoleAvailable(timeoutMs) {
|
|
629
|
-
if (process.platform !== "darwin") throw new Error("Chrome Meet transport with blackhole-2ch audio is currently macOS-only");
|
|
630
|
-
const result = spawnSync(GOOGLE_MEET_SYSTEM_PROFILER_COMMAND, ["SPAudioDataType"], {
|
|
631
|
-
encoding: "utf8",
|
|
632
|
-
timeout: timeoutMs
|
|
633
|
-
});
|
|
634
|
-
const stderr = result.stderr ?? (result.error ? result.error instanceof Error ? result.error.message : String(result.error) : "");
|
|
635
|
-
const output = `${result.stdout ?? ""}\n${stderr}`;
|
|
636
|
-
if ((typeof result.status === "number" ? result.status : result.error ? 1 : 0) !== 0 || !outputMentionsBlackHole2ch(output)) throw new Error("BlackHole 2ch audio device not found on the node.");
|
|
637
|
-
}
|
|
638
|
-
function normalizeMeetKey(value) {
|
|
639
|
-
if (!value) return;
|
|
640
|
-
try {
|
|
641
|
-
const url = new URL(value);
|
|
642
|
-
if (url.hostname.toLowerCase() !== "meet.google.com") return value;
|
|
643
|
-
return /^\/([a-z]{3}-[a-z]{4}-[a-z]{3})(?:$|[/?#])/i.exec(url.pathname)?.[1]?.toLowerCase() ?? value;
|
|
644
|
-
} catch {
|
|
645
|
-
return value;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
const googleMeetNodeHost = createMeetingNodeHost({
|
|
649
|
-
commandName: GOOGLE_MEET_NODE_COMMAND,
|
|
650
|
-
displayName: "Google Meet",
|
|
651
|
-
browserLabel: "Meet",
|
|
652
|
-
bridgeIdPrefix: "meet_node_",
|
|
653
|
-
defaultAudioInputCommand: DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND,
|
|
654
|
-
defaultAudioOutputCommand: DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND,
|
|
655
|
-
talkBackModes: /* @__PURE__ */ new Set([
|
|
656
|
-
"agent",
|
|
657
|
-
"bidi",
|
|
658
|
-
"realtime"
|
|
659
|
-
]),
|
|
660
|
-
agentMode: "agent",
|
|
661
|
-
normalizeUrl: (url) => GOOGLE_MEET_PLATFORM_ADAPTER.urls.validateAndNormalize(url),
|
|
662
|
-
normalizeMeetingKey: normalizeMeetKey,
|
|
663
|
-
assertAudioAvailable: assertBlackHoleAvailable,
|
|
664
|
-
browser: {
|
|
665
|
-
application: "Google Chrome",
|
|
666
|
-
buildProfileArgs: (profile) => ["--args", `--profile-directory=${profile}`],
|
|
667
|
-
openedStatus: "chrome-opened",
|
|
668
|
-
openedNotes: ["Browser page control is handled by OpenClaw browser automation when using chrome-node."]
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
|
-
async function handleGoogleMeetNodeHostCommand(paramsJSON) {
|
|
672
|
-
return await googleMeetNodeHost.handleCommand(paramsJSON);
|
|
673
|
-
}
|
|
674
|
-
//#endregion
|
|
675
|
-
//#region extensions/google-meet/src/node-invoke-policy.ts
|
|
676
|
-
const GOOGLE_MEET_CHROME_NODE_COMMAND = GOOGLE_MEET_NODE_COMMAND;
|
|
677
|
-
const START_MODES = /* @__PURE__ */ new Set([
|
|
678
|
-
"agent",
|
|
679
|
-
"bidi",
|
|
680
|
-
"realtime",
|
|
681
|
-
"transcribe"
|
|
682
|
-
]);
|
|
683
|
-
function createGoogleMeetChromeNodeInvokePolicy(config) {
|
|
684
|
-
return createMeetingBrowserNodeInvokePolicy({
|
|
685
|
-
commandName: GOOGLE_MEET_CHROME_NODE_COMMAND,
|
|
686
|
-
displayName: "Google Meet",
|
|
687
|
-
deniedCode: "GOOGLE_MEET_NODE_POLICY_DENIED",
|
|
688
|
-
supportedModes: START_MODES,
|
|
689
|
-
normalizeUrl: (url) => GOOGLE_MEET_PLATFORM_ADAPTER.urls.validateAndNormalize(url),
|
|
690
|
-
start: config.chrome
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
//#endregion
|
|
694
|
-
//#region extensions/google-meet/src/runtime-probes.ts
|
|
695
|
-
function resolveProbeTimeoutMs(input, fallback) {
|
|
696
|
-
if (input === void 0) return Math.min(Math.max(fallback, 1), 12e4);
|
|
697
|
-
if (!Number.isFinite(input) || input <= 0) throw new Error("timeoutMs must be a positive number");
|
|
698
|
-
return Math.min(Math.trunc(input), 12e4);
|
|
699
|
-
}
|
|
700
|
-
const probes = MeetingPlatformAdapter.createRuntimeProbes({
|
|
701
|
-
defaultSpeechMessage: "Say exactly: Google Meet speech test complete.",
|
|
702
|
-
invalidRequest: (message) => new Error(message),
|
|
703
|
-
resolveTimeoutMs: resolveProbeTimeoutMs,
|
|
704
|
-
shouldWaitForListening: (session) => Boolean((session.transport === "chrome" || session.transport === "chrome-node") && session.chrome?.launched),
|
|
705
|
-
talkBackMode: MeetingPlatformAdapter.isTalkBackMode,
|
|
706
|
-
normalizeUrl: normalizeMeetUrl,
|
|
707
|
-
resolveRequestMode: (mode) => mode === "realtime" ? "agent" : mode,
|
|
708
|
-
defaultTransport: (config) => config.defaultTransport,
|
|
709
|
-
validateListeningTransport: (transport) => {
|
|
710
|
-
if (transport === "twilio") throw new Error("test_listen supports chrome or chrome-node transports");
|
|
711
|
-
},
|
|
712
|
-
resolveSpeechTimeoutMs: (_request, config) => Math.min(config.chrome.joinTimeoutMs, 5e3),
|
|
713
|
-
refreshCaptionHealth: async (context, session) => await context.refreshCaptionHealth(session),
|
|
714
|
-
speechModeError: "test_speech requires mode: agent or bidi; use join mode: transcribe for observe-only sessions.",
|
|
715
|
-
listeningModeError: "test_listen requires mode: transcribe; use test_speech for talk-back sessions."
|
|
716
|
-
});
|
|
717
|
-
const testGoogleMeetListening = probes.testListening;
|
|
718
|
-
const testGoogleMeetSpeech = probes.testSpeech;
|
|
719
|
-
//#endregion
|
|
720
|
-
//#region extensions/google-meet/src/runtime-session.ts
|
|
721
|
-
function resolveTransport(input, config) {
|
|
722
|
-
return input ?? config.defaultTransport;
|
|
723
|
-
}
|
|
724
|
-
function resolveMode(input, config) {
|
|
725
|
-
return input === "realtime" ? "agent" : input ?? config.defaultMode;
|
|
726
|
-
}
|
|
727
|
-
function withSessionAgentConfig(config, agentId) {
|
|
728
|
-
return config.realtime.agentId === agentId ? config : {
|
|
729
|
-
...config,
|
|
730
|
-
realtime: {
|
|
731
|
-
...config.realtime,
|
|
732
|
-
agentId
|
|
733
|
-
}
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
function isBrowserTransport(transport) {
|
|
737
|
-
return transport === "chrome" || transport === "chrome-node";
|
|
738
|
-
}
|
|
739
|
-
function noteSession(session, note) {
|
|
740
|
-
session.notes = [...session.notes.filter((item) => item !== note), note];
|
|
741
|
-
}
|
|
742
|
-
//#endregion
|
|
743
|
-
//#region extensions/google-meet/src/setup.ts
|
|
744
|
-
function resolveUserPath(input) {
|
|
745
|
-
if (input === "~") return os.homedir();
|
|
746
|
-
if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
|
|
747
|
-
return input;
|
|
748
|
-
}
|
|
749
|
-
function isProviderUnreachableWebhookUrl(webhookUrl) {
|
|
750
|
-
try {
|
|
751
|
-
return isBlockedHostnameOrIp(new URL(webhookUrl).hostname);
|
|
752
|
-
} catch {
|
|
753
|
-
return false;
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
function resolveVoiceCallSetupValue(configured, fallback) {
|
|
757
|
-
return normalizeOptionalString(configured) ?? normalizeOptionalString(fallback);
|
|
758
|
-
}
|
|
759
|
-
function getVoiceCallWebhookExposureCheck(voiceCallConfig) {
|
|
760
|
-
const publicUrl = normalizeOptionalString(voiceCallConfig.publicUrl);
|
|
761
|
-
const tunnel = asRecord(voiceCallConfig.tunnel);
|
|
762
|
-
const tailscale = asRecord(voiceCallConfig.tailscale);
|
|
763
|
-
const tunnelProvider = normalizeOptionalString(tunnel.provider);
|
|
764
|
-
const tailscaleMode = normalizeOptionalString(tailscale.mode);
|
|
765
|
-
if (publicUrl) {
|
|
766
|
-
const ok = !isProviderUnreachableWebhookUrl(publicUrl);
|
|
767
|
-
return {
|
|
768
|
-
id: "twilio-voice-call-webhook",
|
|
769
|
-
ok,
|
|
770
|
-
message: ok ? `Voice-call public webhook URL configured: ${publicUrl}` : `Voice-call publicUrl is local/private and cannot be reached by Twilio: ${publicUrl}`
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
if (tunnelProvider && tunnelProvider !== "none") return {
|
|
774
|
-
id: "twilio-voice-call-webhook",
|
|
775
|
-
ok: true,
|
|
776
|
-
message: "Voice-call webhook exposure configured through tunnel"
|
|
777
|
-
};
|
|
778
|
-
if (tailscaleMode && tailscaleMode !== "off") return {
|
|
779
|
-
id: "twilio-voice-call-webhook",
|
|
780
|
-
ok: true,
|
|
781
|
-
message: "Voice-call webhook exposure configured through Tailscale"
|
|
782
|
-
};
|
|
783
|
-
return {
|
|
784
|
-
id: "twilio-voice-call-webhook",
|
|
785
|
-
ok: false,
|
|
786
|
-
message: "Set plugins.entries.voice-call.config.publicUrl or configure voice-call tunnel/tailscale exposure for Twilio dialing"
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
function getGoogleMeetSetupStatus(config, options) {
|
|
790
|
-
const checks = [];
|
|
791
|
-
const env = options?.env ?? process.env;
|
|
792
|
-
const fullConfig = asRecord(options?.fullConfig);
|
|
793
|
-
const mode = options?.mode ?? config.defaultMode;
|
|
794
|
-
const transport = options?.transport ?? config.defaultTransport;
|
|
795
|
-
const needsChromeRealtimeAudio = MeetingPlatformAdapter.isTalkBackMode(mode) && (transport === "chrome" || transport === "chrome-node");
|
|
796
|
-
const pluginEntries = asRecord(asRecord(fullConfig.plugins).entries);
|
|
797
|
-
const pluginAllow = asRecord(fullConfig.plugins).allow;
|
|
798
|
-
const voiceCallEntry = asRecord(pluginEntries["voice-call"]);
|
|
799
|
-
const voiceCallConfig = asRecord(voiceCallEntry.config);
|
|
800
|
-
const voiceCallTwilioConfig = asRecord(voiceCallConfig.twilio);
|
|
801
|
-
if (config.auth.tokenPath) {
|
|
802
|
-
const tokenPath = resolveUserPath(config.auth.tokenPath);
|
|
803
|
-
checks.push({
|
|
804
|
-
id: "google-oauth-token",
|
|
805
|
-
ok: fs.existsSync(tokenPath),
|
|
806
|
-
message: fs.existsSync(tokenPath) ? "Google OAuth token file found" : `Google OAuth token file missing at ${config.auth.tokenPath}`
|
|
807
|
-
});
|
|
808
|
-
} else checks.push({
|
|
809
|
-
id: "google-oauth-token",
|
|
810
|
-
ok: true,
|
|
811
|
-
message: "Google OAuth token path not configured; Chrome profile auth will be used"
|
|
812
|
-
});
|
|
813
|
-
checks.push({
|
|
814
|
-
id: "chrome-profile",
|
|
815
|
-
ok: true,
|
|
816
|
-
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"
|
|
817
|
-
});
|
|
818
|
-
if (needsChromeRealtimeAudio) {
|
|
819
|
-
const hasCommandPair = Boolean(config.chrome.audioInputCommand && config.chrome.audioOutputCommand);
|
|
820
|
-
const hasExternalBridge = Boolean(config.chrome.audioBridgeCommand);
|
|
821
|
-
const agentModeExternalBridgeInvalid = mode === "agent" && hasExternalBridge;
|
|
822
|
-
checks.push({
|
|
823
|
-
id: "audio-bridge",
|
|
824
|
-
ok: mode === "agent" ? hasCommandPair && !agentModeExternalBridgeInvalid : hasExternalBridge || hasCommandPair,
|
|
825
|
-
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"
|
|
826
|
-
});
|
|
827
|
-
} else if (transport === "chrome" || transport === "chrome-node") checks.push({
|
|
828
|
-
id: "audio-bridge",
|
|
829
|
-
ok: true,
|
|
830
|
-
message: "Chrome observe-only mode does not require a realtime audio bridge"
|
|
831
|
-
});
|
|
832
|
-
checks.push({
|
|
833
|
-
id: "guest-join-defaults",
|
|
834
|
-
ok: Boolean(config.chrome.guestName && config.chrome.autoJoin && config.chrome.reuseExistingTab),
|
|
835
|
-
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"
|
|
836
|
-
});
|
|
837
|
-
checks.push({
|
|
838
|
-
id: "chrome-node-target",
|
|
839
|
-
ok: config.defaultTransport !== "chrome-node" || Boolean(config.chromeNode.node),
|
|
840
|
-
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"
|
|
841
|
-
});
|
|
842
|
-
if (needsChromeRealtimeAudio) checks.push({
|
|
843
|
-
id: "intro-after-in-call",
|
|
844
|
-
ok: config.chrome.waitForInCallMs > 0,
|
|
845
|
-
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"
|
|
846
|
-
});
|
|
847
|
-
if (transport === "twilio") {
|
|
848
|
-
const hasRequestDialPlan = Boolean(options?.twilioDialInNumber);
|
|
849
|
-
const hasDefaultDialPlan = Boolean(config.twilio.defaultDialInNumber);
|
|
850
|
-
const hasDialPlan = hasRequestDialPlan || hasDefaultDialPlan;
|
|
851
|
-
checks.push({
|
|
852
|
-
id: "twilio-dial-plan",
|
|
853
|
-
ok: hasDialPlan,
|
|
854
|
-
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"
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
if (config.voiceCall.enabled && (transport === "twilio" || Boolean(config.twilio.defaultDialInNumber) || Object.hasOwn(pluginEntries, "voice-call"))) {
|
|
858
|
-
const voiceCallAllowed = !Array.isArray(pluginAllow) || pluginAllow.includes("voice-call");
|
|
859
|
-
const voiceCallEnabled = Object.hasOwn(pluginEntries, "voice-call") && voiceCallEntry.enabled !== false;
|
|
860
|
-
checks.push({
|
|
861
|
-
id: "twilio-voice-call-plugin",
|
|
862
|
-
ok: voiceCallAllowed && voiceCallEnabled,
|
|
863
|
-
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"
|
|
864
|
-
});
|
|
865
|
-
if ((normalizeOptionalString(voiceCallConfig.provider) ?? "twilio") === "twilio") {
|
|
866
|
-
const accountSid = resolveVoiceCallSetupValue(voiceCallTwilioConfig.accountSid, env.TWILIO_ACCOUNT_SID);
|
|
867
|
-
const authToken = resolveVoiceCallSetupValue(voiceCallTwilioConfig.authToken, env.TWILIO_AUTH_TOKEN);
|
|
868
|
-
const fromNumber = resolveVoiceCallSetupValue(voiceCallConfig.fromNumber, env.TWILIO_FROM_NUMBER);
|
|
869
|
-
const twilioReady = Boolean(accountSid && authToken && fromNumber);
|
|
870
|
-
checks.push({
|
|
871
|
-
id: "twilio-voice-call-credentials",
|
|
872
|
-
ok: twilioReady,
|
|
873
|
-
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"
|
|
874
|
-
});
|
|
875
|
-
checks.push(getVoiceCallWebhookExposureCheck(voiceCallConfig));
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
return createMeetingSetupStatus(checks);
|
|
879
|
-
}
|
|
880
|
-
function addGoogleMeetSetupCheck(status, check) {
|
|
881
|
-
return addMeetingSetupCheck(status, check);
|
|
882
|
-
}
|
|
883
|
-
//#endregion
|
|
884
|
-
//#region extensions/google-meet/src/transports/chrome.ts
|
|
885
|
-
function shouldCaptureCaptions(mode, fullConfig) {
|
|
886
|
-
return mode === "transcribe" || !fullConfig || resolveTranscriptsConfig(fullConfig.transcripts).enabled;
|
|
887
|
-
}
|
|
888
|
-
async function assertBlackHole2chAvailable(params) {
|
|
889
|
-
if (process.platform !== "darwin") throw new Error("Chrome Meet transport with blackhole-2ch audio is currently macOS-only");
|
|
890
|
-
const result = await params.runtime.system.runCommandWithTimeout([GOOGLE_MEET_SYSTEM_PROFILER_COMMAND, "SPAudioDataType"], { timeoutMs: params.timeoutMs });
|
|
891
|
-
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
892
|
-
if (result.code !== 0 || !outputMentionsBlackHole2ch(output)) {
|
|
893
|
-
const hint = params.runtime.system.formatNativeDependencyHint?.({
|
|
894
|
-
packageName: "BlackHole 2ch",
|
|
895
|
-
downloadCommand: "brew install blackhole-2ch"
|
|
896
|
-
}) ?? "";
|
|
897
|
-
throw new Error([
|
|
898
|
-
"BlackHole 2ch audio device not found.",
|
|
899
|
-
"Install BlackHole 2ch and route Chrome input/output through the OpenClaw audio bridge.",
|
|
900
|
-
hint
|
|
901
|
-
].filter(Boolean).join(" "));
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
async function launchChromeMeet(params) {
|
|
905
|
-
const checkRealtimeAudioPrerequisites = async () => {
|
|
906
|
-
if (!MeetingPlatformAdapter.isTalkBackMode(params.mode)) return;
|
|
907
|
-
await assertBlackHole2chAvailable({
|
|
908
|
-
runtime: params.runtime,
|
|
909
|
-
timeoutMs: Math.min(params.config.chrome.joinTimeoutMs, 1e4)
|
|
910
|
-
});
|
|
911
|
-
if (params.config.chrome.audioBridgeHealthCommand) {
|
|
912
|
-
const health = await params.runtime.system.runCommandWithTimeout(params.config.chrome.audioBridgeHealthCommand, { timeoutMs: params.config.chrome.joinTimeoutMs });
|
|
913
|
-
if (health.code !== 0) throw new Error(`Chrome audio bridge health check failed: ${health.stderr || health.stdout || health.code}`);
|
|
914
|
-
}
|
|
915
|
-
};
|
|
916
|
-
const startRealtimeAudioBridge = async () => {
|
|
917
|
-
if (!MeetingPlatformAdapter.isTalkBackMode(params.mode)) return;
|
|
918
|
-
if (params.config.chrome.audioBridgeCommand) {
|
|
919
|
-
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.");
|
|
920
|
-
const bridge = await params.runtime.system.runCommandWithTimeout(params.config.chrome.audioBridgeCommand, { timeoutMs: params.config.chrome.joinTimeoutMs });
|
|
921
|
-
if (bridge.code !== 0) throw new Error(`failed to start Chrome audio bridge: ${bridge.stderr || bridge.stdout || bridge.code}`);
|
|
922
|
-
return { type: "external-command" };
|
|
923
|
-
}
|
|
924
|
-
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.");
|
|
925
|
-
const transport = createLocalMeetingRealtimeAudioTransport({
|
|
926
|
-
inputCommand: params.config.chrome.audioInputCommand,
|
|
927
|
-
outputCommand: params.config.chrome.audioOutputCommand,
|
|
928
|
-
audioFormat: params.config.chrome.audioFormat,
|
|
929
|
-
bargeInInputCommand: params.config.chrome.bargeInInputCommand,
|
|
930
|
-
bargeInRmsThreshold: params.config.chrome.bargeInRmsThreshold,
|
|
931
|
-
bargeInPeakThreshold: params.config.chrome.bargeInPeakThreshold,
|
|
932
|
-
bargeInCooldownMs: params.config.chrome.bargeInCooldownMs,
|
|
933
|
-
logger: params.logger,
|
|
934
|
-
logScope: GOOGLE_MEET_PLATFORM_ADAPTER.logScope
|
|
935
|
-
});
|
|
936
|
-
const bindings = createMeetingRealtimeEngineBindings({
|
|
937
|
-
platform: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
938
|
-
...params
|
|
939
|
-
});
|
|
940
|
-
const engine = params.mode === "agent" ? await startMeetingAgentRealtimeEngine({
|
|
941
|
-
config: params.config,
|
|
942
|
-
fullConfig: params.fullConfig,
|
|
943
|
-
runtime: params.runtime,
|
|
944
|
-
platform: bindings.platform,
|
|
945
|
-
meetingSessionId: params.meetingSessionId,
|
|
946
|
-
requesterSessionKey: params.requesterSessionKey,
|
|
947
|
-
transport,
|
|
948
|
-
logger: params.logger,
|
|
949
|
-
consultAgent: bindings.consultAgent
|
|
950
|
-
}) : await startMeetingRealtimeEngine({
|
|
951
|
-
config: {
|
|
952
|
-
...params.config,
|
|
953
|
-
realtime: {
|
|
954
|
-
...params.config.realtime,
|
|
955
|
-
strategy: "bidi"
|
|
956
|
-
}
|
|
957
|
-
},
|
|
958
|
-
fullConfig: params.fullConfig,
|
|
959
|
-
runtime: params.runtime,
|
|
960
|
-
...bindings,
|
|
961
|
-
meetingSessionId: params.meetingSessionId,
|
|
962
|
-
requesterSessionKey: params.requesterSessionKey,
|
|
963
|
-
transport,
|
|
964
|
-
logger: params.logger
|
|
965
|
-
});
|
|
966
|
-
return {
|
|
967
|
-
type: "command-pair",
|
|
968
|
-
inputCommand: params.config.chrome.audioInputCommand,
|
|
969
|
-
outputCommand: params.config.chrome.audioOutputCommand,
|
|
970
|
-
...engine
|
|
971
|
-
};
|
|
972
|
-
};
|
|
973
|
-
await checkRealtimeAudioPrerequisites();
|
|
974
|
-
if (!params.config.chrome.launch) return {
|
|
975
|
-
launched: false,
|
|
976
|
-
audioBridge: await startRealtimeAudioBridge()
|
|
977
|
-
};
|
|
978
|
-
const result = await openMeetingWithBrowser({
|
|
979
|
-
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
980
|
-
callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
|
|
981
|
-
config: params.config.chrome,
|
|
982
|
-
session: {
|
|
983
|
-
captureCaptions: shouldCaptureCaptions(params.mode, params.fullConfig),
|
|
984
|
-
meetingSessionId: params.meetingSessionId,
|
|
985
|
-
mode: params.mode,
|
|
986
|
-
url: params.url
|
|
987
|
-
}
|
|
988
|
-
});
|
|
989
|
-
const audioBridge = MeetingPlatformAdapter.isTalkBackMode(params.mode) && result.browser?.inCall === true && result.browser.micMuted === false && result.browser.manualAction === void 0 ? await startRealtimeAudioBridge() : void 0;
|
|
990
|
-
return {
|
|
991
|
-
...result,
|
|
992
|
-
audioBridge
|
|
993
|
-
};
|
|
994
|
-
}
|
|
995
|
-
function parseNodeStartResult(raw) {
|
|
996
|
-
const value = raw && typeof raw === "object" && "payload" in raw ? raw.payload : raw;
|
|
997
|
-
if (!value || typeof value !== "object") throw new Error("Google Meet node returned an invalid start result.");
|
|
998
|
-
return value;
|
|
999
|
-
}
|
|
1000
|
-
async function leaveChromeMeet(params) {
|
|
1001
|
-
return await leaveMeetingWithBrowser({
|
|
1002
|
-
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
1003
|
-
callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
|
|
1004
|
-
launch: params.config.chrome.launch,
|
|
1005
|
-
meetingSessionId: params.meetingSessionId,
|
|
1006
|
-
meetingUrl: params.meetingUrl,
|
|
1007
|
-
tab: params.tab,
|
|
1008
|
-
timeoutMs: params.config.chrome.joinTimeoutMs
|
|
1009
|
-
});
|
|
1010
|
-
}
|
|
1011
|
-
async function readChromeMeetTranscript(params) {
|
|
1012
|
-
return await readMeetingTranscriptWithBrowser({
|
|
1013
|
-
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
1014
|
-
callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
|
|
1015
|
-
finalize: params.finalize === true,
|
|
1016
|
-
meetingUrl: params.meetingUrl,
|
|
1017
|
-
meetingSessionId: params.meetingSessionId,
|
|
1018
|
-
tab: params.tab,
|
|
1019
|
-
timeoutMs: Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 1e4)
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
async function readChromeMeetTranscriptOnNode(params) {
|
|
1023
|
-
const nodeId = params.nodeId ?? await resolveChromeNode({
|
|
1024
|
-
runtime: params.runtime,
|
|
1025
|
-
requestedNode: params.config.chromeNode.node
|
|
1026
|
-
});
|
|
1027
|
-
const timeoutMs = Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 1e4);
|
|
1028
|
-
return await readMeetingTranscriptWithBrowser({
|
|
1029
|
-
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
1030
|
-
callBrowser: async (request) => await callBrowserProxyOnNode({
|
|
1031
|
-
runtime: params.runtime,
|
|
1032
|
-
nodeId,
|
|
1033
|
-
method: request.method,
|
|
1034
|
-
path: request.path,
|
|
1035
|
-
body: request.body,
|
|
1036
|
-
timeoutMs: request.timeoutMs
|
|
1037
|
-
}),
|
|
1038
|
-
finalize: params.finalize === true,
|
|
1039
|
-
meetingUrl: params.meetingUrl,
|
|
1040
|
-
meetingSessionId: params.meetingSessionId,
|
|
1041
|
-
tab: params.tab,
|
|
1042
|
-
timeoutMs
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
async function leaveChromeMeetOnNode(params) {
|
|
1046
|
-
const nodeId = params.nodeId ?? await resolveChromeNode({
|
|
1047
|
-
runtime: params.runtime,
|
|
1048
|
-
requestedNode: params.config.chromeNode.node
|
|
1049
|
-
});
|
|
1050
|
-
return await leaveMeetingWithBrowser({
|
|
1051
|
-
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
1052
|
-
callBrowser: async (request) => await callBrowserProxyOnNode({
|
|
1053
|
-
runtime: params.runtime,
|
|
1054
|
-
nodeId,
|
|
1055
|
-
method: request.method,
|
|
1056
|
-
path: request.path,
|
|
1057
|
-
body: request.body,
|
|
1058
|
-
timeoutMs: request.timeoutMs
|
|
1059
|
-
}),
|
|
1060
|
-
launch: params.config.chrome.launch,
|
|
1061
|
-
meetingSessionId: params.meetingSessionId,
|
|
1062
|
-
meetingUrl: params.meetingUrl,
|
|
1063
|
-
tab: params.tab,
|
|
1064
|
-
timeoutMs: params.config.chrome.joinTimeoutMs
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
|
-
async function openMeetWithBrowserProxy(params) {
|
|
1068
|
-
return await openMeetingWithBrowser({
|
|
1069
|
-
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
1070
|
-
callBrowser: async (request) => await callBrowserProxyOnNode({
|
|
1071
|
-
runtime: params.runtime,
|
|
1072
|
-
nodeId: params.nodeId,
|
|
1073
|
-
method: request.method,
|
|
1074
|
-
path: request.path,
|
|
1075
|
-
body: request.body,
|
|
1076
|
-
timeoutMs: request.timeoutMs
|
|
1077
|
-
}),
|
|
1078
|
-
config: params.config.chrome,
|
|
1079
|
-
session: {
|
|
1080
|
-
captureCaptions: params.captureCaptions,
|
|
1081
|
-
mode: params.mode,
|
|
1082
|
-
meetingSessionId: params.meetingSessionId,
|
|
1083
|
-
url: params.url
|
|
1084
|
-
}
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
async function recoverCurrentMeetTab(params) {
|
|
1088
|
-
return {
|
|
1089
|
-
transport: "chrome",
|
|
1090
|
-
...await recoverMeetingBrowserTab({
|
|
1091
|
-
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
1092
|
-
callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
|
|
1093
|
-
captureCaptions: shouldCaptureCaptions(params.mode ?? "bidi", params.fullConfig),
|
|
1094
|
-
config: params.config.chrome,
|
|
1095
|
-
locationLabel: "in local Chrome",
|
|
1096
|
-
mode: params.mode ?? "bidi",
|
|
1097
|
-
readOnly: params.readOnly,
|
|
1098
|
-
requestedMeetingUrl: params.url,
|
|
1099
|
-
trackedMeetingUrl: params.trackedMeetingUrl,
|
|
1100
|
-
trackedTargetId: params.trackedTargetId
|
|
1101
|
-
})
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
async function recoverCurrentMeetTabOnNode(params) {
|
|
1105
|
-
const nodeId = await resolveChromeNode({
|
|
1106
|
-
runtime: params.runtime,
|
|
1107
|
-
requestedNode: params.config.chromeNode.node
|
|
1108
|
-
});
|
|
1109
|
-
return {
|
|
1110
|
-
transport: "chrome-node",
|
|
1111
|
-
nodeId,
|
|
1112
|
-
...await recoverMeetingBrowserTab({
|
|
1113
|
-
adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
1114
|
-
callBrowser: async (request) => await callBrowserProxyOnNode({
|
|
1115
|
-
runtime: params.runtime,
|
|
1116
|
-
nodeId,
|
|
1117
|
-
method: request.method,
|
|
1118
|
-
path: request.path,
|
|
1119
|
-
body: request.body,
|
|
1120
|
-
timeoutMs: request.timeoutMs
|
|
1121
|
-
}),
|
|
1122
|
-
captureCaptions: shouldCaptureCaptions(params.mode ?? "bidi", params.fullConfig),
|
|
1123
|
-
config: params.config.chrome,
|
|
1124
|
-
locationLabel: "on the selected Chrome node",
|
|
1125
|
-
mode: params.mode ?? "bidi",
|
|
1126
|
-
readOnly: params.readOnly,
|
|
1127
|
-
requestedMeetingUrl: params.url,
|
|
1128
|
-
trackedMeetingUrl: params.trackedMeetingUrl,
|
|
1129
|
-
trackedTargetId: params.trackedTargetId
|
|
1130
|
-
})
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
async function launchChromeMeetOnNode(params) {
|
|
1134
|
-
const nodeId = await resolveChromeNode({
|
|
1135
|
-
runtime: params.runtime,
|
|
1136
|
-
requestedNode: params.config.chromeNode.node
|
|
1137
|
-
});
|
|
1138
|
-
try {
|
|
1139
|
-
await params.runtime.nodes.invoke({
|
|
1140
|
-
nodeId,
|
|
1141
|
-
command: GOOGLE_MEET_NODE_COMMAND,
|
|
1142
|
-
params: {
|
|
1143
|
-
action: "stopByUrl",
|
|
1144
|
-
url: params.url,
|
|
1145
|
-
mode: params.mode
|
|
1146
|
-
},
|
|
1147
|
-
timeoutMs: 5e3
|
|
1148
|
-
});
|
|
1149
|
-
} catch (error) {
|
|
1150
|
-
params.logger.debug?.(`[google-meet] node bridge cleanup before join ignored: ${error instanceof Error ? error.message : String(error)}`);
|
|
1151
|
-
}
|
|
1152
|
-
const browserControl = await openMeetWithBrowserProxy({
|
|
1153
|
-
runtime: params.runtime,
|
|
1154
|
-
nodeId,
|
|
1155
|
-
config: params.config,
|
|
1156
|
-
captureCaptions: shouldCaptureCaptions(params.mode, params.fullConfig),
|
|
1157
|
-
mode: params.mode,
|
|
1158
|
-
meetingSessionId: params.meetingSessionId,
|
|
1159
|
-
url: params.url
|
|
1160
|
-
});
|
|
1161
|
-
if (params.config.chrome.launch && MeetingPlatformAdapter.isTalkBackMode(params.mode) && (browserControl.browser?.inCall !== true || browserControl.browser.micMuted !== false || browserControl.browser.manualAction)) return {
|
|
1162
|
-
nodeId,
|
|
1163
|
-
launched: browserControl.launched,
|
|
1164
|
-
browser: browserControl.browser,
|
|
1165
|
-
tab: browserControl.tab
|
|
1166
|
-
};
|
|
1167
|
-
const result = parseNodeStartResult(await params.runtime.nodes.invoke({
|
|
1168
|
-
nodeId,
|
|
1169
|
-
command: GOOGLE_MEET_NODE_COMMAND,
|
|
1170
|
-
params: {
|
|
1171
|
-
action: "start",
|
|
1172
|
-
url: params.url,
|
|
1173
|
-
mode: params.mode,
|
|
1174
|
-
launch: false,
|
|
1175
|
-
browserProfile: params.config.chrome.browserProfile,
|
|
1176
|
-
joinTimeoutMs: params.config.chrome.joinTimeoutMs,
|
|
1177
|
-
audioInputCommand: params.config.chrome.audioInputCommand,
|
|
1178
|
-
audioOutputCommand: params.config.chrome.audioOutputCommand,
|
|
1179
|
-
audioBridgeCommand: params.config.chrome.audioBridgeCommand,
|
|
1180
|
-
audioBridgeHealthCommand: params.config.chrome.audioBridgeHealthCommand
|
|
1181
|
-
},
|
|
1182
|
-
timeoutMs: addTimerTimeoutGraceMs(params.config.chrome.joinTimeoutMs) ?? 1
|
|
1183
|
-
}));
|
|
1184
|
-
if (result.audioBridge?.type === "node-command-pair") {
|
|
1185
|
-
if (!result.bridgeId) throw new Error("Google Meet node did not return an audio bridge id.");
|
|
1186
|
-
const transport = createNodeMeetingRealtimeAudioTransport({
|
|
1187
|
-
runtime: params.runtime,
|
|
1188
|
-
nodeId,
|
|
1189
|
-
bridgeId: result.bridgeId,
|
|
1190
|
-
audioFormat: params.config.chrome.audioFormat,
|
|
1191
|
-
logger: params.logger,
|
|
1192
|
-
commandName: GOOGLE_MEET_NODE_COMMAND,
|
|
1193
|
-
logScope: GOOGLE_MEET_PLATFORM_ADAPTER.logScope,
|
|
1194
|
-
logPrefix: params.mode === "agent" ? "node agent" : "node"
|
|
1195
|
-
});
|
|
1196
|
-
const bindings = createMeetingRealtimeEngineBindings({
|
|
1197
|
-
platform: GOOGLE_MEET_PLATFORM_ADAPTER,
|
|
1198
|
-
...params
|
|
1199
|
-
});
|
|
1200
|
-
const engine = params.mode === "agent" ? await startMeetingAgentRealtimeEngine({
|
|
1201
|
-
config: params.config,
|
|
1202
|
-
fullConfig: params.fullConfig,
|
|
1203
|
-
runtime: params.runtime,
|
|
1204
|
-
platform: bindings.platform,
|
|
1205
|
-
meetingSessionId: params.meetingSessionId,
|
|
1206
|
-
requesterSessionKey: params.requesterSessionKey,
|
|
1207
|
-
logPrefix: "node",
|
|
1208
|
-
transport,
|
|
1209
|
-
logger: params.logger,
|
|
1210
|
-
consultAgent: bindings.consultAgent
|
|
1211
|
-
}) : await startMeetingRealtimeEngine({
|
|
1212
|
-
config: {
|
|
1213
|
-
...params.config,
|
|
1214
|
-
realtime: {
|
|
1215
|
-
...params.config.realtime,
|
|
1216
|
-
strategy: "bidi"
|
|
1217
|
-
}
|
|
1218
|
-
},
|
|
1219
|
-
fullConfig: params.fullConfig,
|
|
1220
|
-
runtime: params.runtime,
|
|
1221
|
-
...bindings,
|
|
1222
|
-
meetingSessionId: params.meetingSessionId,
|
|
1223
|
-
requesterSessionKey: params.requesterSessionKey,
|
|
1224
|
-
logPrefix: "node",
|
|
1225
|
-
talkSessionId: `google-meet:${params.meetingSessionId}:${result.bridgeId}:node-realtime`,
|
|
1226
|
-
talkContext: {
|
|
1227
|
-
nodeId,
|
|
1228
|
-
bridgeId: result.bridgeId
|
|
1229
|
-
},
|
|
1230
|
-
transport,
|
|
1231
|
-
logger: params.logger
|
|
1232
|
-
});
|
|
1233
|
-
const bridge = {
|
|
1234
|
-
type: "node-command-pair",
|
|
1235
|
-
nodeId,
|
|
1236
|
-
bridgeId: result.bridgeId,
|
|
1237
|
-
...engine
|
|
1238
|
-
};
|
|
1239
|
-
return {
|
|
1240
|
-
nodeId,
|
|
1241
|
-
launched: browserControl.launched || result.launched === true,
|
|
1242
|
-
audioBridge: bridge,
|
|
1243
|
-
browser: browserControl.browser ?? result.browser,
|
|
1244
|
-
tab: browserControl.tab
|
|
1245
|
-
};
|
|
1246
|
-
}
|
|
1247
|
-
if (result.audioBridge?.type === "external-command") return {
|
|
1248
|
-
nodeId,
|
|
1249
|
-
launched: browserControl.launched || result.launched === true,
|
|
1250
|
-
audioBridge: { type: "external-command" },
|
|
1251
|
-
browser: browserControl.browser ?? result.browser,
|
|
1252
|
-
tab: browserControl.tab
|
|
1253
|
-
};
|
|
1254
|
-
return {
|
|
1255
|
-
nodeId,
|
|
1256
|
-
launched: browserControl.launched || result.launched === true,
|
|
1257
|
-
browser: browserControl.browser ?? result.browser,
|
|
1258
|
-
tab: browserControl.tab
|
|
1259
|
-
};
|
|
1260
|
-
}
|
|
1261
|
-
//#endregion
|
|
1262
|
-
//#region extensions/google-meet/src/runtime-setup.ts
|
|
1263
|
-
function collectChromeAudioCommands(config) {
|
|
1264
|
-
return uniqueStrings((config.chrome.audioBridgeCommand ? [config.chrome.audioBridgeCommand[0]] : [
|
|
1265
|
-
config.chrome.audioInputCommand?.[0],
|
|
1266
|
-
config.chrome.audioOutputCommand?.[0],
|
|
1267
|
-
config.chrome.bargeInInputCommand?.[0]
|
|
1268
|
-
]).filter((value) => Boolean(value?.trim())));
|
|
1269
|
-
}
|
|
1270
|
-
async function commandExists(runtime, command) {
|
|
1271
|
-
return (await runtime.system.runCommandWithTimeout([
|
|
1272
|
-
"/bin/sh",
|
|
1273
|
-
"-lc",
|
|
1274
|
-
"command -v \"$1\" >/dev/null 2>&1",
|
|
1275
|
-
"sh",
|
|
1276
|
-
command
|
|
1277
|
-
], { timeoutMs: 5e3 })).code === 0;
|
|
1278
|
-
}
|
|
1279
|
-
async function getGoogleMeetRuntimeSetupStatus(params) {
|
|
1280
|
-
const options = params.options ?? {};
|
|
1281
|
-
const transport = options.transport ?? params.config.defaultTransport;
|
|
1282
|
-
const mode = options.mode === "realtime" ? "agent" : options.mode ?? params.config.defaultMode;
|
|
1283
|
-
const twilioDialInNumber = transport === "twilio" ? normalizeDialInNumber(options.dialInNumber) : void 0;
|
|
1284
|
-
const shouldCheckChromeNode = transport === "chrome-node" || !options.transport && Boolean(params.config.chromeNode.node);
|
|
1285
|
-
let status = getGoogleMeetSetupStatus(params.config, {
|
|
1286
|
-
fullConfig: params.fullConfig,
|
|
1287
|
-
mode,
|
|
1288
|
-
transport,
|
|
1289
|
-
twilioDialInNumber
|
|
1290
|
-
});
|
|
1291
|
-
if (shouldCheckChromeNode) try {
|
|
1292
|
-
const node = await resolveChromeNodeInfo({
|
|
1293
|
-
runtime: params.runtime,
|
|
1294
|
-
requestedNode: params.config.chromeNode.node
|
|
1295
|
-
});
|
|
1296
|
-
const label = node.displayName ?? node.remoteIp ?? node.nodeId ?? "connected node";
|
|
1297
|
-
status = addGoogleMeetSetupCheck(status, {
|
|
1298
|
-
id: "chrome-node-connected",
|
|
1299
|
-
ok: true,
|
|
1300
|
-
message: `Connected Google Meet node ready: ${label}`
|
|
1301
|
-
});
|
|
1302
|
-
} catch (error) {
|
|
1303
|
-
status = addGoogleMeetSetupCheck(status, {
|
|
1304
|
-
id: "chrome-node-connected",
|
|
1305
|
-
ok: false,
|
|
1306
|
-
message: formatErrorMessage(error)
|
|
1307
|
-
});
|
|
1308
|
-
}
|
|
1309
|
-
if (transport !== "chrome" || mode !== "agent" && mode !== "bidi") return status;
|
|
1310
|
-
try {
|
|
1311
|
-
await assertBlackHole2chAvailable({
|
|
1312
|
-
runtime: params.runtime,
|
|
1313
|
-
timeoutMs: Math.min(params.config.chrome.joinTimeoutMs, 1e4)
|
|
1314
|
-
});
|
|
1315
|
-
status = addGoogleMeetSetupCheck(status, {
|
|
1316
|
-
id: "chrome-local-audio-device",
|
|
1317
|
-
ok: true,
|
|
1318
|
-
message: "BlackHole 2ch audio device found"
|
|
1319
|
-
});
|
|
1320
|
-
} catch (error) {
|
|
1321
|
-
status = addGoogleMeetSetupCheck(status, {
|
|
1322
|
-
id: "chrome-local-audio-device",
|
|
1323
|
-
ok: false,
|
|
1324
|
-
message: formatErrorMessage(error)
|
|
1325
|
-
});
|
|
1326
|
-
}
|
|
1327
|
-
const commands = collectChromeAudioCommands(params.config);
|
|
1328
|
-
const missingCommands = [];
|
|
1329
|
-
for (const command of commands) try {
|
|
1330
|
-
if (!await commandExists(params.runtime, command)) missingCommands.push(command);
|
|
1331
|
-
} catch {
|
|
1332
|
-
missingCommands.push(command);
|
|
1333
|
-
}
|
|
1334
|
-
return addGoogleMeetSetupCheck(status, {
|
|
1335
|
-
id: "chrome-local-audio-commands",
|
|
1336
|
-
ok: commands.length > 0 && missingCommands.length === 0,
|
|
1337
|
-
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(", ")}`
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1340
|
-
//#endregion
|
|
1341
|
-
//#region extensions/google-meet/src/voice-call-gateway.ts
|
|
1342
|
-
const GOOGLE_MEET_VOICE_CALL_SURFACE = {
|
|
1343
|
-
clientDisplayName: "Google Meet plugin",
|
|
1344
|
-
configPath: "google-meet voiceCall.gatewayUrl",
|
|
1345
|
-
logScope: "[google-meet]",
|
|
1346
|
-
meetingLabel: "Meet",
|
|
1347
|
-
providerLabel: "Twilio"
|
|
1348
|
-
};
|
|
1349
|
-
async function createConnectedGatewayClient(params) {
|
|
1350
|
-
let client;
|
|
1351
|
-
await new Promise((resolve, reject) => {
|
|
1352
|
-
const abortStart = new AbortController();
|
|
1353
|
-
const timer = setTimeout(() => {
|
|
1354
|
-
abortStart.abort();
|
|
1355
|
-
reject(/* @__PURE__ */ new Error("gateway connect timeout"));
|
|
1356
|
-
}, params.config.requestTimeoutMs);
|
|
1357
|
-
client = new GatewayClient({
|
|
1358
|
-
url: params.config.gatewayUrl,
|
|
1359
|
-
token: params.config.token,
|
|
1360
|
-
requestTimeoutMs: params.config.requestTimeoutMs,
|
|
1361
|
-
clientName: "cli",
|
|
1362
|
-
clientDisplayName: params.surface.clientDisplayName,
|
|
1363
|
-
scopes: ["operator.write"],
|
|
1364
|
-
onHelloOk: () => {
|
|
1365
|
-
clearTimeout(timer);
|
|
1366
|
-
resolve();
|
|
1367
|
-
},
|
|
1368
|
-
onConnectError: (error) => {
|
|
1369
|
-
clearTimeout(timer);
|
|
1370
|
-
abortStart.abort();
|
|
1371
|
-
reject(error);
|
|
1372
|
-
}
|
|
1373
|
-
});
|
|
1374
|
-
startGatewayClientWhenEventLoopReady(client, {
|
|
1375
|
-
timeoutMs: params.config.requestTimeoutMs,
|
|
1376
|
-
signal: abortStart.signal
|
|
1377
|
-
}).then((readiness) => {
|
|
1378
|
-
if (!readiness.ready && !readiness.aborted) {
|
|
1379
|
-
clearTimeout(timer);
|
|
1380
|
-
reject(/* @__PURE__ */ new Error("gateway event loop readiness timeout"));
|
|
1381
|
-
}
|
|
1382
|
-
}).catch((error) => {
|
|
1383
|
-
clearTimeout(timer);
|
|
1384
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1385
|
-
});
|
|
1386
|
-
});
|
|
1387
|
-
return client;
|
|
1388
|
-
}
|
|
1389
|
-
function createVoiceCallGateway(params) {
|
|
1390
|
-
return createMeetingVoiceCallGateway({
|
|
1391
|
-
config: params.config.voiceCall,
|
|
1392
|
-
runtime: params.runtime,
|
|
1393
|
-
surface: GOOGLE_MEET_VOICE_CALL_SURFACE,
|
|
1394
|
-
connectClient: createConnectedGatewayClient
|
|
1395
|
-
});
|
|
1396
|
-
}
|
|
1397
|
-
const isVoiceCallMissingError = isMeetingVoiceCallMissingError;
|
|
1398
|
-
async function joinMeetViaVoiceCallGateway(params) {
|
|
1399
|
-
return await joinMeetingViaVoiceCallGateway({
|
|
1400
|
-
...params,
|
|
1401
|
-
config: params.config.voiceCall,
|
|
1402
|
-
surface: GOOGLE_MEET_VOICE_CALL_SURFACE
|
|
1403
|
-
});
|
|
1404
|
-
}
|
|
1405
|
-
async function endMeetVoiceCallGatewayCall(params) {
|
|
1406
|
-
await endMeetingVoiceCallGatewayCall(params);
|
|
1407
|
-
}
|
|
1408
|
-
async function getMeetVoiceCallGatewayCall(params) {
|
|
1409
|
-
return await getMeetingVoiceCallGatewayCall(params);
|
|
1410
|
-
}
|
|
1411
|
-
async function speakMeetViaVoiceCallGateway(params) {
|
|
1412
|
-
await speakMeetingViaVoiceCallGateway(params);
|
|
1413
|
-
}
|
|
1414
|
-
//#endregion
|
|
1415
|
-
//#region extensions/google-meet/src/runtime.ts
|
|
1416
|
-
const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1417
|
-
var GoogleMeetRuntime = class {
|
|
1418
|
-
#createdBrowserTabs;
|
|
1419
|
-
#agentId;
|
|
1420
|
-
#voiceCallGateway;
|
|
1421
|
-
#sessions;
|
|
1422
|
-
constructor(params) {
|
|
1423
|
-
this.params = params;
|
|
1424
|
-
this.#createdBrowserTabs = /* @__PURE__ */ new Map();
|
|
1425
|
-
this.transcriptSourceRuntime = () => this.#sessions;
|
|
1426
|
-
const adapter = GOOGLE_MEET_PLATFORM_ADAPTER;
|
|
1427
|
-
this.#agentId = resolveDefaultAgentId(params.fullConfig);
|
|
1428
|
-
this.#voiceCallGateway = createVoiceCallGateway(params);
|
|
1429
|
-
this.#sessions = new MeetingSessionRuntime({
|
|
1430
|
-
logger: params.logger,
|
|
1431
|
-
logScope: "[google-meet]",
|
|
1432
|
-
formatError: formatErrorMessage,
|
|
1433
|
-
reuseExistingBrowserTab: params.config.chrome.reuseExistingTab,
|
|
1434
|
-
waitForInCallMs: params.config.chrome.waitForInCallMs,
|
|
1435
|
-
joinTimeoutMs: params.config.chrome.joinTimeoutMs,
|
|
1436
|
-
defaultSpeechInstructions: params.config.realtime.introMessage,
|
|
1437
|
-
transientSpeechBlockedReasons: /* @__PURE__ */ new Set([
|
|
1438
|
-
"not-in-call",
|
|
1439
|
-
"browser-unverified",
|
|
1440
|
-
"meet-microphone-muted"
|
|
1441
|
-
]),
|
|
1442
|
-
messages: {
|
|
1443
|
-
previousBrowserLeaveFailed: "Could not leave the previous Meet browser tab before reassignment.",
|
|
1444
|
-
reassignedSessionNote: "Ended before the same Meet tab was reassigned to another agent.",
|
|
1445
|
-
reusedSessionNote: "Reused existing active Meet session.",
|
|
1446
|
-
replacementBrowserLeaveFailed: "Could not leave the previous Meet browser tab before reassignment.",
|
|
1447
|
-
speechBlockedFallback: "Realtime speech blocked until Google Meet is ready.",
|
|
1448
|
-
speech: {
|
|
1449
|
-
audioBridgeUnavailable: "Realtime speech requires an active Chrome audio bridge.",
|
|
1450
|
-
browserUnverified: "Google Meet browser state has not been verified yet.",
|
|
1451
|
-
microphoneMuted: "Turn on the OpenClaw Google Meet microphone before asking OpenClaw to speak.",
|
|
1452
|
-
microphoneMutedReason: "meet-microphone-muted",
|
|
1453
|
-
notInCall: "Google Meet has not reported that the browser participant is in the call.",
|
|
1454
|
-
notInCallReason: "not-in-call",
|
|
1455
|
-
browserUnverifiedReason: "browser-unverified",
|
|
1456
|
-
audioBridgeUnavailableReason: "audio-bridge-unavailable"
|
|
1457
|
-
}
|
|
1458
|
-
},
|
|
1459
|
-
resolveJoin: (request) => ({
|
|
1460
|
-
url: adapter.urls.validateAndNormalize(request.url),
|
|
1461
|
-
transport: resolveTransport(request.transport, params.config),
|
|
1462
|
-
mode: resolveMode(request.mode, params.config),
|
|
1463
|
-
agentId: normalizeAgentId(request.agentId ?? params.config.realtime.agentId ?? this.#agentId)
|
|
1464
|
-
}),
|
|
1465
|
-
createSession: ({ resolved, createdAt }) => createMeetingSession({
|
|
1466
|
-
platform: adapter,
|
|
1467
|
-
config: params.config,
|
|
1468
|
-
resolved,
|
|
1469
|
-
createdAt
|
|
1470
|
-
}),
|
|
1471
|
-
resolveSpeechInstructions: (request) => request.message ?? params.config.realtime.introMessage,
|
|
1472
|
-
isBrowserTransport,
|
|
1473
|
-
isTalkBackMode: (mode) => MeetingPlatformAdapter.isTalkBackMode(mode),
|
|
1474
|
-
isTranscribeMode: (mode) => mode === "transcribe",
|
|
1475
|
-
sameMeetingUrl: (left, right) => adapter.urls.isSameMeeting(left, right),
|
|
1476
|
-
normalizeMeetingUrlForReuse: (url) => adapter.urls.normalizeForReuse(url),
|
|
1477
|
-
getBrowser: (session) => session.chrome ? {
|
|
1478
|
-
launched: session.chrome.launched,
|
|
1479
|
-
nodeId: session.chrome.nodeId,
|
|
1480
|
-
tab: session.chrome.browserTab,
|
|
1481
|
-
health: session.chrome.health,
|
|
1482
|
-
hasAudioBridge: Boolean(session.chrome.audioBridge)
|
|
1483
|
-
} : void 0,
|
|
1484
|
-
setBrowserTab: (session, tab) => {
|
|
1485
|
-
if (session.chrome) session.chrome.browserTab = tab;
|
|
1486
|
-
},
|
|
1487
|
-
setBrowserHealth: (session, health) => {
|
|
1488
|
-
if (session.chrome) session.chrome.health = health;
|
|
1489
|
-
},
|
|
1490
|
-
joinTransport: async ({ request, session, context }) => await this.#joinTransport(request, session, context),
|
|
1491
|
-
releaseBrowserTab: async (session) => await this.#releaseBrowserTab(session),
|
|
1492
|
-
refreshBrowserHealth: async (session, options) => await this.#refreshBrowserHealth(session, options),
|
|
1493
|
-
refreshStatus: async (session) => await this.#refreshStatus(session),
|
|
1494
|
-
refreshReusableSession: async (session, _request, _resolved) => {
|
|
1495
|
-
if (session.transport === "twilio") await this.#refreshTwilioVoiceCallStatus(session);
|
|
1496
|
-
},
|
|
1497
|
-
ensureRealtimeBridge: async (session) => await this.#ensureChromeRealtimeBridge(session),
|
|
1498
|
-
captureTranscript: async (session, options) => await this.#captureTranscript(session, options),
|
|
1499
|
-
speakViaTransport: async (session, instructions) => await this.#speakViaTransport(session, instructions),
|
|
1500
|
-
durableTranscripts: {
|
|
1501
|
-
config: params.fullConfig.transcripts,
|
|
1502
|
-
providerId: "google-meet",
|
|
1503
|
-
providerName: "Google Meet"
|
|
1504
|
-
}
|
|
1505
|
-
});
|
|
1506
|
-
}
|
|
1507
|
-
list() {
|
|
1508
|
-
return this.#sessions.list();
|
|
1509
|
-
}
|
|
1510
|
-
async status(sessionId) {
|
|
1511
|
-
return await this.#sessions.status(sessionId);
|
|
1512
|
-
}
|
|
1513
|
-
async transcript(sessionId, options = {}) {
|
|
1514
|
-
return await this.#sessions.transcript(sessionId, options);
|
|
1515
|
-
}
|
|
1516
|
-
async setupStatus(options = {}) {
|
|
1517
|
-
return await getGoogleMeetRuntimeSetupStatus({
|
|
1518
|
-
config: this.params.config,
|
|
1519
|
-
fullConfig: this.params.fullConfig,
|
|
1520
|
-
runtime: this.params.runtime,
|
|
1521
|
-
options
|
|
1522
|
-
});
|
|
1523
|
-
}
|
|
1524
|
-
async createViaBrowser() {
|
|
1525
|
-
const result = await GOOGLE_MEET_PLATFORM_ADAPTER.create.browser({
|
|
1526
|
-
runtime: this.params.runtime,
|
|
1527
|
-
config: this.params.config
|
|
1528
|
-
});
|
|
1529
|
-
if (result.openedByPlugin && result.targetId) this.#createdBrowserTabs.set(`${result.nodeId}:${result.targetId}`, result.meetingUri);
|
|
1530
|
-
return result;
|
|
1531
|
-
}
|
|
1532
|
-
async recoverCurrentTab(request = {}) {
|
|
1533
|
-
const transport = resolveTransport(request.transport, this.params.config);
|
|
1534
|
-
if (transport === "twilio") throw new Error("recover_current_tab only supports chrome or chrome-node transports");
|
|
1535
|
-
const url = request.url ? GOOGLE_MEET_PLATFORM_ADAPTER.urls.validateAndNormalize(request.url) : void 0;
|
|
1536
|
-
return transport === "chrome-node" ? await recoverCurrentMeetTabOnNode({
|
|
1537
|
-
runtime: this.params.runtime,
|
|
1538
|
-
config: this.params.config,
|
|
1539
|
-
fullConfig: this.params.fullConfig,
|
|
1540
|
-
url
|
|
1541
|
-
}) : await recoverCurrentMeetTab({
|
|
1542
|
-
runtime: this.params.runtime,
|
|
1543
|
-
config: this.params.config,
|
|
1544
|
-
fullConfig: this.params.fullConfig,
|
|
1545
|
-
url
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
1548
|
-
async join(request) {
|
|
1549
|
-
return await this.#sessions.join(request);
|
|
1550
|
-
}
|
|
1551
|
-
async leave(sessionId, options) {
|
|
1552
|
-
return await this.#sessions.leave(sessionId, options);
|
|
1553
|
-
}
|
|
1554
|
-
async speak(sessionId, instructions) {
|
|
1555
|
-
return await this.#sessions.speak(sessionId, instructions);
|
|
1556
|
-
}
|
|
1557
|
-
async testSpeech(request) {
|
|
1558
|
-
return await testGoogleMeetSpeech(this.#probeContext(), request);
|
|
1559
|
-
}
|
|
1560
|
-
async testListen(request) {
|
|
1561
|
-
return await testGoogleMeetListening(this.#probeContext(), request);
|
|
1562
|
-
}
|
|
1563
|
-
#probeContext() {
|
|
1564
|
-
return {
|
|
1565
|
-
config: this.params.config,
|
|
1566
|
-
resolveAgentId: (request) => normalizeAgentId(request.agentId ?? this.params.config.realtime.agentId ?? this.#agentId),
|
|
1567
|
-
list: () => this.list(),
|
|
1568
|
-
join: async (request) => await this.join(request),
|
|
1569
|
-
isReusable: (session, resolved) => this.#sessions.isReusableSession(session, resolved),
|
|
1570
|
-
hasHealthHandle: (sessionId) => this.#sessions.hasHealthHandle(sessionId),
|
|
1571
|
-
refreshHealth: (sessionId) => this.#sessions.refreshHealth(sessionId),
|
|
1572
|
-
refreshCaptionHealth: async (session) => await this.#sessions.refreshCaptionHealth(session)
|
|
1573
|
-
};
|
|
1574
|
-
}
|
|
1575
|
-
async #joinTransport(request, session, context) {
|
|
1576
|
-
if (isBrowserTransport(session.transport)) {
|
|
1577
|
-
const chromeConfig = withSessionAgentConfig(this.params.config, session.agentId);
|
|
1578
|
-
const result = session.transport === "chrome-node" ? await launchChromeMeetOnNode({
|
|
1579
|
-
runtime: this.params.runtime,
|
|
1580
|
-
config: chromeConfig,
|
|
1581
|
-
fullConfig: this.params.fullConfig,
|
|
1582
|
-
meetingSessionId: session.id,
|
|
1583
|
-
requesterSessionKey: request.requesterSessionKey,
|
|
1584
|
-
mode: session.mode,
|
|
1585
|
-
url: session.url,
|
|
1586
|
-
logger: this.params.logger
|
|
1587
|
-
}) : await launchChromeMeet({
|
|
1588
|
-
runtime: this.params.runtime,
|
|
1589
|
-
config: chromeConfig,
|
|
1590
|
-
fullConfig: this.params.fullConfig,
|
|
1591
|
-
meetingSessionId: session.id,
|
|
1592
|
-
requesterSessionKey: request.requesterSessionKey,
|
|
1593
|
-
mode: session.mode,
|
|
1594
|
-
url: session.url,
|
|
1595
|
-
logger: this.params.logger
|
|
1596
|
-
});
|
|
1597
|
-
const nodeId = "nodeId" in result ? result.nodeId : void 0;
|
|
1598
|
-
let tab = result.tab;
|
|
1599
|
-
const createdKey = session.transport === "chrome-node" && nodeId && tab ? `${nodeId}:${tab.targetId}` : void 0;
|
|
1600
|
-
const createdUrl = createdKey ? this.#createdBrowserTabs.get(createdKey) : void 0;
|
|
1601
|
-
if (createdKey) this.#createdBrowserTabs.delete(createdKey);
|
|
1602
|
-
if (tab && GOOGLE_MEET_PLATFORM_ADAPTER.urls.isSameMeeting(createdUrl, session.url)) tab = {
|
|
1603
|
-
...tab,
|
|
1604
|
-
openedByPlugin: true
|
|
1605
|
-
};
|
|
1606
|
-
tab = context.inheritedBrowserTab({
|
|
1607
|
-
session,
|
|
1608
|
-
transport: session.transport,
|
|
1609
|
-
nodeId,
|
|
1610
|
-
meetingUrl: session.url,
|
|
1611
|
-
tab
|
|
1612
|
-
});
|
|
1613
|
-
session.chrome = {
|
|
1614
|
-
audioBackend: this.params.config.chrome.audioBackend,
|
|
1615
|
-
launched: result.launched,
|
|
1616
|
-
nodeId,
|
|
1617
|
-
browserProfile: this.params.config.chrome.browserProfile,
|
|
1618
|
-
browserTab: tab,
|
|
1619
|
-
health: result.browser
|
|
1620
|
-
};
|
|
1621
|
-
const handles = this.#attachChromeAudioBridge(session, result.audioBridge);
|
|
1622
|
-
if (handles) context.attachRuntimeHandles(session, handles);
|
|
1623
|
-
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 joins as the signed-in Google profile and expects BlackHole 2ch audio routing." : "Chrome transport joins as the signed-in Google profile without starting the realtime audio bridge.");
|
|
1624
|
-
this.#sessions.refreshSpeechReadiness(session);
|
|
1625
|
-
return {};
|
|
1626
|
-
}
|
|
1627
|
-
const dialPlan = GOOGLE_MEET_PLATFORM_ADAPTER.dialIn.buildPlan({
|
|
1628
|
-
dialInNumber: request.dialInNumber,
|
|
1629
|
-
defaultDialInNumber: this.params.config.twilio.defaultDialInNumber,
|
|
1630
|
-
pin: request.pin,
|
|
1631
|
-
defaultPin: this.params.config.twilio.defaultPin,
|
|
1632
|
-
dtmfSequence: request.dtmfSequence,
|
|
1633
|
-
defaultDtmfSequence: this.params.config.twilio.defaultDtmfSequence,
|
|
1634
|
-
dtmfDelayMs: this.params.config.voiceCall.dtmfDelayMs
|
|
1635
|
-
});
|
|
1636
|
-
const dialInNumber = dialPlan.number;
|
|
1637
|
-
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.");
|
|
1638
|
-
const dtmfSequence = dialPlan.dtmfSequence;
|
|
1639
|
-
const delegatedAgentId = Boolean(normalizeOptionalString(request.agentId) || normalizeOptionalString(this.params.config.realtime.agentId)) ? session.agentId : void 0;
|
|
1640
|
-
const voiceCallResult = this.params.config.voiceCall.enabled ? await joinMeetViaVoiceCallGateway({
|
|
1641
|
-
config: this.params.config,
|
|
1642
|
-
gateway: this.#voiceCallGateway,
|
|
1643
|
-
dialInNumber,
|
|
1644
|
-
dtmfSequence,
|
|
1645
|
-
logger: this.params.logger,
|
|
1646
|
-
...request.requesterSessionKey ? { requesterSessionKey: request.requesterSessionKey } : {},
|
|
1647
|
-
agentId: delegatedAgentId,
|
|
1648
|
-
sessionKey: delegatedAgentId ? `agent:${delegatedAgentId}:google-meet:${session.id}` : `voice:google-meet:${session.id}`,
|
|
1649
|
-
message: MeetingPlatformAdapter.isTalkBackMode(session.mode) ? request.message ?? this.params.config.voiceCall.introMessage ?? this.params.config.realtime.introMessage : void 0
|
|
1650
|
-
}) : void 0;
|
|
1651
|
-
session.twilio = {
|
|
1652
|
-
dialInNumber,
|
|
1653
|
-
pinProvided: Boolean(dialPlan.pin),
|
|
1654
|
-
dtmfSequence,
|
|
1655
|
-
voiceCallId: voiceCallResult?.callId,
|
|
1656
|
-
dtmfSent: voiceCallResult?.dtmfSent,
|
|
1657
|
-
introSent: voiceCallResult?.introSent
|
|
1658
|
-
};
|
|
1659
|
-
if (voiceCallResult?.callId) context.attachRuntimeHandles(session, { stop: async () => {
|
|
1660
|
-
await endMeetVoiceCallGatewayCall({
|
|
1661
|
-
gateway: this.#voiceCallGateway,
|
|
1662
|
-
callId: voiceCallResult.callId
|
|
1663
|
-
});
|
|
1664
|
-
} });
|
|
1665
|
-
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.");
|
|
1666
|
-
return { delegatedSpoken: Boolean(voiceCallResult?.introSent) };
|
|
1667
|
-
}
|
|
1668
|
-
#attachChromeAudioBridge(session, audioBridge) {
|
|
1669
|
-
if (!session.chrome || !audioBridge) return;
|
|
1670
|
-
session.chrome.audioBridge = {
|
|
1671
|
-
type: audioBridge.type,
|
|
1672
|
-
provider: audioBridge.type === "command-pair" || audioBridge.type === "node-command-pair" ? audioBridge.providerId : void 0
|
|
1673
|
-
};
|
|
1674
|
-
return audioBridge.type === "command-pair" || audioBridge.type === "node-command-pair" ? {
|
|
1675
|
-
stop: audioBridge.stop,
|
|
1676
|
-
speak: audioBridge.speak,
|
|
1677
|
-
getHealth: audioBridge.getHealth
|
|
1678
|
-
} : void 0;
|
|
1679
|
-
}
|
|
1680
|
-
async #ensureChromeRealtimeBridge(session) {
|
|
1681
|
-
if (!MeetingPlatformAdapter.isTalkBackMode(session.mode) || !isBrowserTransport(session.transport) || session.state !== "active" || !session.chrome || session.chrome.audioBridge || session.chrome.health?.inCall !== true || session.chrome.health.micMuted !== false || session.chrome.health.manualAction) return;
|
|
1682
|
-
const config = withSessionAgentConfig(this.params.config, session.agentId);
|
|
1683
|
-
const recoveryConfig = {
|
|
1684
|
-
...config,
|
|
1685
|
-
chrome: {
|
|
1686
|
-
...config.chrome,
|
|
1687
|
-
launch: false
|
|
1688
|
-
},
|
|
1689
|
-
...session.chrome.nodeId ? { chromeNode: {
|
|
1690
|
-
...config.chromeNode,
|
|
1691
|
-
node: session.chrome.nodeId
|
|
1692
|
-
} } : {}
|
|
1693
|
-
};
|
|
1694
|
-
const result = session.transport === "chrome-node" ? await launchChromeMeetOnNode({
|
|
1695
|
-
runtime: this.params.runtime,
|
|
1696
|
-
config: recoveryConfig,
|
|
1697
|
-
fullConfig: this.params.fullConfig,
|
|
1698
|
-
meetingSessionId: session.id,
|
|
1699
|
-
mode: session.mode,
|
|
1700
|
-
url: session.url,
|
|
1701
|
-
logger: this.params.logger
|
|
1702
|
-
}) : await launchChromeMeet({
|
|
1703
|
-
runtime: this.params.runtime,
|
|
1704
|
-
config: recoveryConfig,
|
|
1705
|
-
fullConfig: this.params.fullConfig,
|
|
1706
|
-
meetingSessionId: session.id,
|
|
1707
|
-
mode: session.mode,
|
|
1708
|
-
url: session.url,
|
|
1709
|
-
logger: this.params.logger
|
|
1710
|
-
});
|
|
1711
|
-
session.updatedAt = nowIso();
|
|
1712
|
-
return this.#attachChromeAudioBridge(session, result.audioBridge);
|
|
1713
|
-
}
|
|
1714
|
-
async #refreshBrowserHealth(session, options = {}) {
|
|
1715
|
-
try {
|
|
1716
|
-
const result = session.transport === "chrome-node" ? await recoverCurrentMeetTabOnNode({
|
|
1717
|
-
runtime: this.params.runtime,
|
|
1718
|
-
config: this.params.config,
|
|
1719
|
-
fullConfig: this.params.fullConfig,
|
|
1720
|
-
mode: session.mode,
|
|
1721
|
-
readOnly: options.readOnly,
|
|
1722
|
-
trackedMeetingUrl: session.url,
|
|
1723
|
-
trackedTargetId: session.chrome?.browserTab?.targetId,
|
|
1724
|
-
url: session.url
|
|
1725
|
-
}) : await recoverCurrentMeetTab({
|
|
1726
|
-
runtime: this.params.runtime,
|
|
1727
|
-
config: this.params.config,
|
|
1728
|
-
fullConfig: this.params.fullConfig,
|
|
1729
|
-
mode: session.mode,
|
|
1730
|
-
readOnly: options.readOnly,
|
|
1731
|
-
trackedMeetingUrl: session.url,
|
|
1732
|
-
trackedTargetId: session.chrome?.browserTab?.targetId,
|
|
1733
|
-
url: session.url
|
|
1734
|
-
});
|
|
1735
|
-
if (result.found && session.chrome) {
|
|
1736
|
-
if (result.targetId) {
|
|
1737
|
-
const currentTab = session.chrome.browserTab;
|
|
1738
|
-
session.chrome.browserTab = {
|
|
1739
|
-
targetId: result.targetId,
|
|
1740
|
-
openedByPlugin: result.targetId === currentTab?.targetId ? currentTab.openedByPlugin : false
|
|
1741
|
-
};
|
|
1742
|
-
}
|
|
1743
|
-
if (result.browser) session.chrome.health = {
|
|
1744
|
-
...session.chrome.health,
|
|
1745
|
-
...result.browser
|
|
1746
|
-
};
|
|
1747
|
-
session.updatedAt = nowIso();
|
|
1748
|
-
}
|
|
1749
|
-
} catch (error) {
|
|
1750
|
-
this.params.logger.debug?.(`[google-meet] browser readiness refresh ignored: ${formatErrorMessage(error)}`);
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
async #refreshStatus(session) {
|
|
1754
|
-
if (isBrowserTransport(session.transport)) await this.#sessions.refreshBrowserHealth(session, {
|
|
1755
|
-
force: true,
|
|
1756
|
-
readOnly: true
|
|
1757
|
-
});
|
|
1758
|
-
else if (session.transport === "twilio") await this.#refreshTwilioVoiceCallStatus(session);
|
|
1759
|
-
else this.#sessions.refreshSpeechReadiness(session);
|
|
1760
|
-
}
|
|
1761
|
-
async #refreshTwilioVoiceCallStatus(session) {
|
|
1762
|
-
const callId = session.twilio?.voiceCallId;
|
|
1763
|
-
if (!callId || session.state !== "active") {
|
|
1764
|
-
this.#sessions.refreshSpeechReadiness(session);
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
1767
|
-
try {
|
|
1768
|
-
if ((await getMeetVoiceCallGatewayCall({
|
|
1769
|
-
gateway: this.#voiceCallGateway,
|
|
1770
|
-
callId
|
|
1771
|
-
})).found === false) this.#sessions.markSessionEnded(session, "Voice Call is no longer active.");
|
|
1772
|
-
} catch (error) {
|
|
1773
|
-
this.params.logger.debug?.(`[google-meet] voice-call status refresh ignored: ${formatErrorMessage(error)}`);
|
|
1774
|
-
}
|
|
1775
|
-
this.#sessions.refreshSpeechReadiness(session);
|
|
1776
|
-
}
|
|
1777
|
-
async #speakViaTransport(session, instructions) {
|
|
1778
|
-
if (session.transport !== "twilio" || !session.twilio?.voiceCallId) return;
|
|
1779
|
-
try {
|
|
1780
|
-
await speakMeetViaVoiceCallGateway({
|
|
1781
|
-
gateway: this.#voiceCallGateway,
|
|
1782
|
-
callId: session.twilio.voiceCallId,
|
|
1783
|
-
message: instructions || this.params.config.voiceCall.introMessage || this.params.config.realtime.introMessage || ""
|
|
1784
|
-
});
|
|
1785
|
-
} catch (error) {
|
|
1786
|
-
if (!isVoiceCallMissingError(error)) throw error;
|
|
1787
|
-
this.#sessions.markSessionEnded(session, "Voice Call is no longer active.");
|
|
1788
|
-
return {
|
|
1789
|
-
handled: true,
|
|
1790
|
-
spoken: false
|
|
1791
|
-
};
|
|
1792
|
-
}
|
|
1793
|
-
session.twilio.introSent = true;
|
|
1794
|
-
session.updatedAt = nowIso();
|
|
1795
|
-
return {
|
|
1796
|
-
handled: true,
|
|
1797
|
-
spoken: true
|
|
1798
|
-
};
|
|
1799
|
-
}
|
|
1800
|
-
async #captureTranscript(session, options = {}) {
|
|
1801
|
-
const tab = session.chrome?.browserTab;
|
|
1802
|
-
if (!tab) return;
|
|
1803
|
-
return session.transport === "chrome-node" ? await readChromeMeetTranscriptOnNode({
|
|
1804
|
-
runtime: this.params.runtime,
|
|
1805
|
-
nodeId: session.chrome?.nodeId,
|
|
1806
|
-
config: this.params.config,
|
|
1807
|
-
...options.finalize === void 0 ? {} : { finalize: options.finalize },
|
|
1808
|
-
meetingUrl: session.url,
|
|
1809
|
-
meetingSessionId: session.id,
|
|
1810
|
-
tab
|
|
1811
|
-
}) : await readChromeMeetTranscript({
|
|
1812
|
-
runtime: this.params.runtime,
|
|
1813
|
-
config: this.params.config,
|
|
1814
|
-
...options.finalize === void 0 ? {} : { finalize: options.finalize },
|
|
1815
|
-
meetingUrl: session.url,
|
|
1816
|
-
meetingSessionId: session.id,
|
|
1817
|
-
tab
|
|
1818
|
-
});
|
|
1819
|
-
}
|
|
1820
|
-
async #releaseBrowserTab(session) {
|
|
1821
|
-
if (!isBrowserTransport(session.transport)) return;
|
|
1822
|
-
const tab = session.chrome?.browserTab;
|
|
1823
|
-
if (!tab) {
|
|
1824
|
-
noteSession(session, "No tracked Meet browser tab for this session; close the Meet tab manually if it is still in the call.");
|
|
1825
|
-
session.browserLeft = false;
|
|
1826
|
-
return false;
|
|
1827
|
-
}
|
|
1828
|
-
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)) {
|
|
1829
|
-
noteSession(session, "Kept the shared Meet tab open because another active session uses it.");
|
|
1830
|
-
session.browserLeft = void 0;
|
|
1831
|
-
return;
|
|
1832
|
-
}
|
|
1833
|
-
let left;
|
|
1834
|
-
try {
|
|
1835
|
-
const result = session.transport === "chrome-node" ? await leaveChromeMeetOnNode({
|
|
1836
|
-
runtime: this.params.runtime,
|
|
1837
|
-
nodeId: session.chrome?.nodeId,
|
|
1838
|
-
config: this.params.config,
|
|
1839
|
-
meetingSessionId: session.id,
|
|
1840
|
-
meetingUrl: session.url,
|
|
1841
|
-
tab
|
|
1842
|
-
}) : await leaveChromeMeet({
|
|
1843
|
-
runtime: this.params.runtime,
|
|
1844
|
-
config: this.params.config,
|
|
1845
|
-
meetingSessionId: session.id,
|
|
1846
|
-
meetingUrl: session.url,
|
|
1847
|
-
tab
|
|
1848
|
-
});
|
|
1849
|
-
noteSession(session, result.note);
|
|
1850
|
-
left = result.left;
|
|
1851
|
-
} catch (error) {
|
|
1852
|
-
noteSession(session, `Browser control could not leave the Meet tab: ${formatErrorMessage(error)}`);
|
|
1853
|
-
left = false;
|
|
1854
|
-
}
|
|
1855
|
-
if (session.chrome && left) {
|
|
1856
|
-
session.chrome.browserTab = void 0;
|
|
1857
|
-
if (session.chrome.health) session.chrome.health = {
|
|
1858
|
-
...session.chrome.health,
|
|
1859
|
-
captioning: false,
|
|
1860
|
-
audioOutputRouted: false,
|
|
1861
|
-
providerConnected: false,
|
|
1862
|
-
realtimeReady: false,
|
|
1863
|
-
audioInputActive: false,
|
|
1864
|
-
audioOutputActive: false
|
|
1865
|
-
};
|
|
1866
|
-
}
|
|
1867
|
-
session.browserLeft = left;
|
|
1868
|
-
return left;
|
|
1869
|
-
}
|
|
1870
|
-
};
|
|
1871
|
-
//#endregion
|
|
1872
|
-
//#region extensions/google-meet/src/plugin-helpers.ts
|
|
1873
|
-
const loadGoogleMeetCreateModule = createLazyRuntimeModule(() => import("./create-af1MDlus.js").then((n) => n.t));
|
|
1874
|
-
const loadGoogleMeetCliModule = createLazyRuntimeModule(() => import("./cli-GeuAaEIy.js"));
|
|
1875
|
-
function asParamRecord(params) {
|
|
1876
|
-
return params && typeof params === "object" && !Array.isArray(params) ? params : {};
|
|
1877
|
-
}
|
|
1878
|
-
function normalizeTransport(value) {
|
|
1879
|
-
return value === "chrome" || value === "chrome-node" || value === "twilio" ? value : void 0;
|
|
1880
|
-
}
|
|
1881
|
-
function normalizeMode(value) {
|
|
1882
|
-
if (value === "realtime") return "agent";
|
|
1883
|
-
return value === "agent" || value === "bidi" || value === "transcribe" ? value : void 0;
|
|
1884
|
-
}
|
|
1885
|
-
function resolveMeetingInput(config, value) {
|
|
1886
|
-
const meeting = normalizeOptionalString(value) ?? config.defaults.meeting;
|
|
1887
|
-
if (!meeting) throw new Error("Meeting input is required");
|
|
1888
|
-
return meeting;
|
|
1889
|
-
}
|
|
1890
|
-
function shouldJoinCreatedMeet(raw) {
|
|
1891
|
-
return raw.join !== false && raw.join !== "false";
|
|
1892
|
-
}
|
|
1893
|
-
const googleMeetToolDeps = {
|
|
1894
|
-
callGatewayFromCli,
|
|
1895
|
-
platform: () => process.platform
|
|
1896
|
-
};
|
|
1897
|
-
const testing = {
|
|
1898
|
-
setCallGatewayFromCliForTests(next) {
|
|
1899
|
-
googleMeetToolDeps.callGatewayFromCli = next ?? callGatewayFromCli;
|
|
1900
|
-
},
|
|
1901
|
-
setPlatformForTests(next) {
|
|
1902
|
-
googleMeetToolDeps.platform = next ?? (() => process.platform);
|
|
1903
|
-
},
|
|
1904
|
-
isGoogleMeetAgentToolActionUnsupportedOnHost,
|
|
1905
|
-
resolveGoogleMeetGatewayOperationTimeoutMs
|
|
1906
|
-
};
|
|
1907
|
-
function googleMeetGatewayMethodForToolAction(action) {
|
|
1908
|
-
switch (action) {
|
|
1909
|
-
case "recover_current_tab": return "googlemeet.recoverCurrentTab";
|
|
1910
|
-
case "setup_status": return "googlemeet.setup";
|
|
1911
|
-
case "test_speech": return "googlemeet.testSpeech";
|
|
1912
|
-
case "test_listen": return "googlemeet.testListen";
|
|
1913
|
-
case "end_active_conference": return "googlemeet.endActiveConference";
|
|
1914
|
-
default: return `googlemeet.${action}`;
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
function isGoogleMeetAgentToolActionUnsupportedOnHost(params) {
|
|
1918
|
-
if ((params.platform ?? googleMeetToolDeps.platform()) === "darwin") return false;
|
|
1919
|
-
const action = params.raw.action;
|
|
1920
|
-
if (action !== "join" && action !== "test_speech" && !(action === "create" && shouldJoinCreatedMeet(params.raw))) return false;
|
|
1921
|
-
const transport = normalizeTransport(params.raw.transport) ?? params.config.defaultTransport;
|
|
1922
|
-
const mode = action === "test_speech" ? "agent" : normalizeMode(params.raw.mode) ?? params.config.defaultMode;
|
|
1923
|
-
return transport === "chrome" && MeetingPlatformAdapter.isTalkBackMode(mode);
|
|
1924
|
-
}
|
|
1925
|
-
function assertGoogleMeetAgentToolActionSupported(params) {
|
|
1926
|
-
if (!isGoogleMeetAgentToolActionUnsupportedOnHost(params)) return;
|
|
1927
|
-
throw new Error("Google Meet local Chrome talk-back audio is macOS-only. On this host, use mode: transcribe, transport: twilio, or transport: chrome-node backed by a macOS node.");
|
|
1928
|
-
}
|
|
1929
|
-
function readGatewayErrorDetails(err) {
|
|
1930
|
-
if (!err || typeof err !== "object" || !("details" in err)) return;
|
|
1931
|
-
return err.details;
|
|
1932
|
-
}
|
|
1933
|
-
async function callGoogleMeetGatewayFromTool(params) {
|
|
1934
|
-
try {
|
|
1935
|
-
if (params.runtime) return await params.runtime.gateway.request(googleMeetGatewayMethodForToolAction(params.action), params.raw, {
|
|
1936
|
-
timeoutMs: resolveGoogleMeetGatewayOperationTimeoutMs(params.config),
|
|
1937
|
-
scopes: ["operator.admin"]
|
|
1938
|
-
});
|
|
1939
|
-
return await googleMeetToolDeps.callGatewayFromCli(googleMeetGatewayMethodForToolAction(params.action), {
|
|
1940
|
-
json: true,
|
|
1941
|
-
timeout: String(resolveGoogleMeetGatewayOperationTimeoutMs(params.config))
|
|
1942
|
-
}, params.raw, {
|
|
1943
|
-
progress: false,
|
|
1944
|
-
scopes: ["operator.admin"]
|
|
1945
|
-
});
|
|
1946
|
-
} catch (err) {
|
|
1947
|
-
const details = readGatewayErrorDetails(err);
|
|
1948
|
-
if (details && typeof details === "object") return details;
|
|
1949
|
-
throw err;
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
function keepTrustedToolAgentId(raw, client) {
|
|
1953
|
-
const { agentId: rawAgentId, ...rest } = raw;
|
|
1954
|
-
if (client?.internal?.pluginRuntimeOwnerId !== "google-meet") return rest;
|
|
1955
|
-
const agentId = normalizeOptionalString(rawAgentId);
|
|
1956
|
-
return agentId ? {
|
|
1957
|
-
...rest,
|
|
1958
|
-
agentId
|
|
1959
|
-
} : rest;
|
|
1960
|
-
}
|
|
1961
|
-
async function createMeetFromParams(params) {
|
|
1962
|
-
return (await loadGoogleMeetCreateModule()).createMeetFromParams(params);
|
|
1963
|
-
}
|
|
1964
|
-
async function createAndJoinMeetFromParams(params) {
|
|
1965
|
-
return (await loadGoogleMeetCreateModule()).createAndJoinMeetFromParams(params);
|
|
1966
|
-
}
|
|
1967
|
-
async function resolveGoogleMeetTokenFromParams(config, raw) {
|
|
1968
|
-
const { resolveGoogleMeetAccessToken } = await import("./oauth-DOfMKlGp.js");
|
|
1969
|
-
return resolveGoogleMeetAccessToken({
|
|
1970
|
-
clientId: normalizeOptionalString(raw.clientId) ?? config.oauth.clientId,
|
|
1971
|
-
clientSecret: normalizeOptionalString(raw.clientSecret) ?? config.oauth.clientSecret,
|
|
1972
|
-
refreshToken: normalizeOptionalString(raw.refreshToken) ?? config.oauth.refreshToken,
|
|
1973
|
-
accessToken: normalizeOptionalString(raw.accessToken) ?? config.oauth.accessToken,
|
|
1974
|
-
expiresAt: typeof raw.expiresAt === "number" ? raw.expiresAt : config.oauth.expiresAt
|
|
1975
|
-
});
|
|
1976
|
-
}
|
|
1977
|
-
function wantsCalendarLookup(raw) {
|
|
1978
|
-
return raw.today === true || Boolean(normalizeOptionalString(raw.event));
|
|
1979
|
-
}
|
|
1980
|
-
async function resolveMeetingFromParams(params) {
|
|
1981
|
-
if (wantsCalendarLookup(params.raw)) {
|
|
1982
|
-
const window = params.raw.today === true ? buildGoogleMeetCalendarDayWindow() : {};
|
|
1983
|
-
const calendarEvent = await findGoogleMeetCalendarEvent({
|
|
1984
|
-
accessToken: params.accessToken,
|
|
1985
|
-
calendarId: normalizeOptionalString(params.raw.calendarId),
|
|
1986
|
-
eventQuery: normalizeOptionalString(params.raw.event),
|
|
1987
|
-
...window
|
|
1988
|
-
});
|
|
1989
|
-
return {
|
|
1990
|
-
meeting: calendarEvent.meetingUri,
|
|
1991
|
-
calendarEvent
|
|
1992
|
-
};
|
|
1993
|
-
}
|
|
1994
|
-
return { meeting: resolveMeetingInput(params.config, params.raw.meeting) };
|
|
1995
|
-
}
|
|
1996
|
-
async function resolveSpaceFromParams(config, raw) {
|
|
1997
|
-
const token = await resolveGoogleMeetTokenFromParams(config, raw);
|
|
1998
|
-
const { meeting, calendarEvent } = await resolveMeetingFromParams({
|
|
1999
|
-
config,
|
|
2000
|
-
raw,
|
|
2001
|
-
accessToken: token.accessToken
|
|
2002
|
-
});
|
|
2003
|
-
return {
|
|
2004
|
-
meeting,
|
|
2005
|
-
token,
|
|
2006
|
-
space: await fetchGoogleMeetSpace({
|
|
2007
|
-
accessToken: token.accessToken,
|
|
2008
|
-
meeting
|
|
2009
|
-
}),
|
|
2010
|
-
calendarEvent
|
|
2011
|
-
};
|
|
2012
|
-
}
|
|
2013
|
-
async function resolveArtifactQueryFromParams(config, raw) {
|
|
2014
|
-
const meeting = normalizeOptionalString(raw.meeting) ?? config.defaults.meeting;
|
|
2015
|
-
const conferenceRecord = normalizeOptionalString(raw.conferenceRecord);
|
|
2016
|
-
const token = await resolveGoogleMeetTokenFromParams(config, raw);
|
|
2017
|
-
const resolvedMeeting = conferenceRecord ? { meeting } : wantsCalendarLookup(raw) ? await resolveMeetingFromParams({
|
|
2018
|
-
config,
|
|
2019
|
-
raw,
|
|
2020
|
-
accessToken: token.accessToken
|
|
2021
|
-
}) : { meeting };
|
|
2022
|
-
if (!resolvedMeeting.meeting && !conferenceRecord) throw new Error("Meeting input, calendar lookup, or conferenceRecord required");
|
|
2023
|
-
return {
|
|
2024
|
-
token,
|
|
2025
|
-
meeting: resolvedMeeting.meeting,
|
|
2026
|
-
calendarEvent: resolvedMeeting.calendarEvent,
|
|
2027
|
-
conferenceRecord,
|
|
2028
|
-
pageSize: readPositiveIntegerParam(raw, "pageSize"),
|
|
2029
|
-
includeTranscriptEntries: raw.includeTranscriptEntries !== false,
|
|
2030
|
-
includeDocumentBodies: raw.includeDocumentBodies === true,
|
|
2031
|
-
allConferenceRecords: raw.includeAllConferenceRecords === true,
|
|
2032
|
-
mergeDuplicateParticipants: raw.mergeDuplicateParticipants !== false,
|
|
2033
|
-
lateAfterMinutes: readPositiveIntegerParam(raw, "lateAfterMinutes"),
|
|
2034
|
-
earlyBeforeMinutes: readPositiveIntegerParam(raw, "earlyBeforeMinutes")
|
|
2035
|
-
};
|
|
2036
|
-
}
|
|
2037
|
-
async function exportGoogleMeetBundleFromParams(config, raw) {
|
|
2038
|
-
const resolved = await resolveArtifactQueryFromParams(config, raw);
|
|
2039
|
-
const [artifacts, attendance] = await Promise.all([fetchGoogleMeetArtifacts({
|
|
2040
|
-
accessToken: resolved.token.accessToken,
|
|
2041
|
-
meeting: resolved.meeting,
|
|
2042
|
-
conferenceRecord: resolved.conferenceRecord,
|
|
2043
|
-
pageSize: resolved.pageSize,
|
|
2044
|
-
includeTranscriptEntries: resolved.includeTranscriptEntries,
|
|
2045
|
-
includeDocumentBodies: resolved.includeDocumentBodies,
|
|
2046
|
-
allConferenceRecords: resolved.allConferenceRecords
|
|
2047
|
-
}), fetchGoogleMeetAttendance({
|
|
2048
|
-
accessToken: resolved.token.accessToken,
|
|
2049
|
-
meeting: resolved.meeting,
|
|
2050
|
-
conferenceRecord: resolved.conferenceRecord,
|
|
2051
|
-
pageSize: resolved.pageSize,
|
|
2052
|
-
allConferenceRecords: resolved.allConferenceRecords,
|
|
2053
|
-
mergeDuplicateParticipants: resolved.mergeDuplicateParticipants,
|
|
2054
|
-
lateAfterMinutes: resolved.lateAfterMinutes,
|
|
2055
|
-
earlyBeforeMinutes: resolved.earlyBeforeMinutes
|
|
2056
|
-
})]);
|
|
2057
|
-
const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } = await loadGoogleMeetCliModule();
|
|
2058
|
-
const calendarId = normalizeOptionalString(raw.calendarId);
|
|
2059
|
-
const request = {
|
|
2060
|
-
...resolved.meeting ? { meeting: resolved.meeting } : {},
|
|
2061
|
-
...resolved.conferenceRecord ? { conferenceRecord: resolved.conferenceRecord } : {},
|
|
2062
|
-
...resolved.calendarEvent?.event.id ? { calendarEventId: resolved.calendarEvent.event.id } : {},
|
|
2063
|
-
...resolved.calendarEvent?.event.summary ? { calendarEventSummary: resolved.calendarEvent.event.summary } : {},
|
|
2064
|
-
...calendarId ? { calendarId } : {},
|
|
2065
|
-
...resolved.pageSize !== void 0 ? { pageSize: resolved.pageSize } : {},
|
|
2066
|
-
includeTranscriptEntries: resolved.includeTranscriptEntries,
|
|
2067
|
-
includeDocumentBodies: resolved.includeDocumentBodies,
|
|
2068
|
-
allConferenceRecords: resolved.allConferenceRecords,
|
|
2069
|
-
mergeDuplicateParticipants: resolved.mergeDuplicateParticipants,
|
|
2070
|
-
...resolved.lateAfterMinutes !== void 0 ? { lateAfterMinutes: resolved.lateAfterMinutes } : {},
|
|
2071
|
-
...resolved.earlyBeforeMinutes !== void 0 ? { earlyBeforeMinutes: resolved.earlyBeforeMinutes } : {}
|
|
2072
|
-
};
|
|
2073
|
-
const tokenSource = resolved.token.refreshed ? "refresh-token" : "cached-access-token";
|
|
2074
|
-
if (raw.dryRun === true) return {
|
|
2075
|
-
dryRun: true,
|
|
2076
|
-
manifest: buildGoogleMeetExportManifest({
|
|
2077
|
-
artifacts,
|
|
2078
|
-
attendance,
|
|
2079
|
-
files: googleMeetExportFileNames(),
|
|
2080
|
-
request,
|
|
2081
|
-
tokenSource,
|
|
2082
|
-
...resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {}
|
|
2083
|
-
}),
|
|
2084
|
-
...resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {},
|
|
2085
|
-
tokenSource
|
|
2086
|
-
};
|
|
2087
|
-
const outputDir = normalizeOptionalString(raw.outputDir) ?? normalizeOptionalString(raw.output);
|
|
2088
|
-
return {
|
|
2089
|
-
...await writeMeetExportBundle({
|
|
2090
|
-
...outputDir ? { outputDir } : {},
|
|
2091
|
-
artifacts,
|
|
2092
|
-
attendance,
|
|
2093
|
-
zip: raw.zip === true,
|
|
2094
|
-
request,
|
|
2095
|
-
tokenSource,
|
|
2096
|
-
...resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {}
|
|
2097
|
-
}),
|
|
2098
|
-
...resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {},
|
|
2099
|
-
tokenSource
|
|
2100
|
-
};
|
|
2101
|
-
}
|
|
2102
|
-
function createGoogleMeetRuntimeAccessor(params) {
|
|
2103
|
-
let runtime = null;
|
|
2104
|
-
return async () => {
|
|
2105
|
-
if (!params.config.enabled) throw new Error("Google Meet plugin disabled in plugin config");
|
|
2106
|
-
if (!runtime) runtime = new GoogleMeetRuntime({
|
|
2107
|
-
config: params.config,
|
|
2108
|
-
fullConfig: params.api.config,
|
|
2109
|
-
runtime: params.api.runtime,
|
|
2110
|
-
logger: params.api.logger
|
|
2111
|
-
});
|
|
2112
|
-
return runtime;
|
|
2113
|
-
};
|
|
2114
|
-
}
|
|
2115
|
-
function formatGoogleMeetGatewayError(err) {
|
|
2116
|
-
return isGoogleMeetBrowserManualActionError(err) ? err.payload : { error: formatErrorMessage(err) };
|
|
2117
|
-
}
|
|
2118
|
-
function sendGoogleMeetGatewayError(respond, err, code = ErrorCodes.UNAVAILABLE) {
|
|
2119
|
-
const payload = formatGoogleMeetGatewayError(err);
|
|
2120
|
-
respond(false, payload, errorShape(code, typeof payload.error === "string" ? payload.error : "Google Meet request failed", { details: payload }));
|
|
2121
|
-
}
|
|
2122
|
-
//#endregion
|
|
2123
10
|
//#region extensions/google-meet/src/plugin-schema.ts
|
|
2124
|
-
const googleMeetConfigSchema = {
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
},
|
|
2128
|
-
uiHints: {
|
|
2129
|
-
"defaults.meeting": {
|
|
2130
|
-
label: "Default Meeting",
|
|
2131
|
-
help: "Meet URL, meeting code, or spaces/{id} used when CLI commands omit a meeting."
|
|
2132
|
-
},
|
|
2133
|
-
"preview.enrollmentAcknowledged": {
|
|
2134
|
-
label: "Preview Acknowledged",
|
|
2135
|
-
help: "Confirms you understand the Google Meet Media API is still Developer Preview.",
|
|
2136
|
-
advanced: true
|
|
2137
|
-
},
|
|
2138
|
-
defaultTransport: {
|
|
2139
|
-
label: "Default Transport",
|
|
2140
|
-
help: "Chrome uses a signed-in browser profile. Chrome-node runs Chrome on a paired node. Twilio uses Meet dial-in numbers."
|
|
2141
|
-
},
|
|
2142
|
-
defaultMode: {
|
|
2143
|
-
label: "Default Mode",
|
|
2144
|
-
help: "Agent uses realtime transcription plus regular OpenClaw TTS. Bidi uses the realtime voice model directly. Transcribe observes only."
|
|
2145
|
-
},
|
|
2146
|
-
"chrome.audioBackend": {
|
|
2147
|
-
label: "Chrome Audio Backend",
|
|
2148
|
-
help: "BlackHole 2ch is required for local duplex audio routing."
|
|
2149
|
-
},
|
|
2150
|
-
"chrome.launch": { label: "Launch Chrome" },
|
|
2151
|
-
"chrome.browserProfile": {
|
|
2152
|
-
label: "Chrome Profile",
|
|
2153
|
-
advanced: true
|
|
2154
|
-
},
|
|
2155
|
-
"chrome.guestName": {
|
|
2156
|
-
label: "Guest Name",
|
|
2157
|
-
help: "Used when Chrome lands on the signed-out Meet guest-name screen."
|
|
2158
|
-
},
|
|
2159
|
-
"chrome.reuseExistingTab": {
|
|
2160
|
-
label: "Reuse Existing Meet Tab",
|
|
2161
|
-
help: "Avoids opening duplicate tabs for the same Meet URL."
|
|
2162
|
-
},
|
|
2163
|
-
"chrome.autoJoin": {
|
|
2164
|
-
label: "Auto Join Guest Screen",
|
|
2165
|
-
help: "Best-effort guest-name fill and Join Now click through OpenClaw browser automation."
|
|
2166
|
-
},
|
|
2167
|
-
"chrome.waitForInCallMs": {
|
|
2168
|
-
label: "Wait For In-Call (ms)",
|
|
2169
|
-
help: "Waits for Chrome to report that the Meet tab is in-call before the realtime intro speaks.",
|
|
2170
|
-
advanced: true
|
|
2171
|
-
},
|
|
2172
|
-
"chrome.audioFormat": {
|
|
2173
|
-
label: "Audio Format",
|
|
2174
|
-
help: "Command-pair audio format. PCM16 24 kHz is the default Chrome/Meet path; G.711 mu-law 8 kHz remains available for legacy command pairs.",
|
|
2175
|
-
advanced: true
|
|
2176
|
-
},
|
|
2177
|
-
"chrome.audioBufferBytes": {
|
|
2178
|
-
label: "Audio Buffer Bytes",
|
|
2179
|
-
help: "SoX processing buffer for generated Chrome command-pair audio commands. Lower values reduce latency but may underrun on busy hosts.",
|
|
2180
|
-
advanced: true
|
|
2181
|
-
},
|
|
2182
|
-
"chrome.audioInputCommand": {
|
|
2183
|
-
label: "Audio Input Command",
|
|
2184
|
-
help: "Command that writes meeting audio to stdout in chrome.audioFormat.",
|
|
2185
|
-
advanced: true
|
|
2186
|
-
},
|
|
2187
|
-
"chrome.audioOutputCommand": {
|
|
2188
|
-
label: "Audio Output Command",
|
|
2189
|
-
help: "Command that reads assistant audio from stdin in chrome.audioFormat.",
|
|
2190
|
-
advanced: true
|
|
2191
|
-
},
|
|
2192
|
-
"chrome.bargeInInputCommand": {
|
|
2193
|
-
label: "Barge-In Input Command",
|
|
2194
|
-
help: "Optional Gateway-hosted microphone command that writes signed 16-bit little-endian mono PCM for human interruption detection while assistant playback is active.",
|
|
2195
|
-
advanced: true
|
|
2196
|
-
},
|
|
2197
|
-
"chrome.bargeInRmsThreshold": {
|
|
2198
|
-
label: "Barge-In RMS Threshold",
|
|
2199
|
-
help: "RMS level on chrome.bargeInInputCommand that counts as a human interruption.",
|
|
2200
|
-
advanced: true
|
|
2201
|
-
},
|
|
2202
|
-
"chrome.bargeInPeakThreshold": {
|
|
2203
|
-
label: "Barge-In Peak Threshold",
|
|
2204
|
-
help: "Peak level on chrome.bargeInInputCommand that counts as a human interruption.",
|
|
2205
|
-
advanced: true
|
|
2206
|
-
},
|
|
2207
|
-
"chrome.bargeInCooldownMs": {
|
|
2208
|
-
label: "Barge-In Cooldown (ms)",
|
|
2209
|
-
help: "Minimum delay between repeated barge-in clears.",
|
|
2210
|
-
advanced: true
|
|
2211
|
-
},
|
|
2212
|
-
"chrome.audioBridgeCommand": {
|
|
2213
|
-
label: "Audio Bridge Command",
|
|
2214
|
-
advanced: true
|
|
2215
|
-
},
|
|
2216
|
-
"chrome.audioBridgeHealthCommand": {
|
|
2217
|
-
label: "Audio Bridge Health Command",
|
|
2218
|
-
advanced: true
|
|
2219
|
-
},
|
|
2220
|
-
"chromeNode.node": {
|
|
2221
|
-
label: "Chrome Node",
|
|
2222
|
-
help: "Node id/name/IP that owns Chrome, BlackHole, and SoX for chrome-node transport.",
|
|
2223
|
-
advanced: true
|
|
2224
|
-
},
|
|
2225
|
-
"twilio.defaultDialInNumber": {
|
|
2226
|
-
label: "Default Dial-In Number",
|
|
2227
|
-
placeholder: "+15551234567"
|
|
2228
|
-
},
|
|
2229
|
-
"twilio.defaultPin": {
|
|
2230
|
-
label: "Default PIN",
|
|
2231
|
-
advanced: true
|
|
2232
|
-
},
|
|
2233
|
-
"twilio.defaultDtmfSequence": {
|
|
2234
|
-
label: "Default DTMF Sequence",
|
|
2235
|
-
advanced: true
|
|
2236
|
-
},
|
|
2237
|
-
"voiceCall.enabled": { label: "Delegate To Voice Call" },
|
|
2238
|
-
"voiceCall.gatewayUrl": {
|
|
2239
|
-
label: "Voice Call Gateway URL",
|
|
2240
|
-
advanced: true
|
|
2241
|
-
},
|
|
2242
|
-
"voiceCall.token": {
|
|
2243
|
-
label: "Voice Call Gateway Token",
|
|
2244
|
-
sensitive: true,
|
|
2245
|
-
advanced: true
|
|
2246
|
-
},
|
|
2247
|
-
"voiceCall.requestTimeoutMs": {
|
|
2248
|
-
label: "Voice Call Request Timeout (ms)",
|
|
2249
|
-
advanced: true
|
|
2250
|
-
},
|
|
2251
|
-
"voiceCall.dtmfDelayMs": {
|
|
2252
|
-
label: "DTMF Wait Before PIN (ms)",
|
|
2253
|
-
help: "Leading Twilio wait time before playing a PIN-derived Meet DTMF sequence. Increase it if Meet asks for the PIN after DTMF was sent.",
|
|
2254
|
-
advanced: true
|
|
2255
|
-
},
|
|
2256
|
-
"voiceCall.postDtmfSpeechDelayMs": {
|
|
2257
|
-
label: "Post-DTMF Speech Delay (ms)",
|
|
2258
|
-
help: "Delay before requesting the realtime intro greeting after Voice Call starts the Twilio leg.",
|
|
2259
|
-
advanced: true
|
|
2260
|
-
},
|
|
2261
|
-
"voiceCall.introMessage": {
|
|
2262
|
-
label: "Voice Call Intro Message",
|
|
2263
|
-
advanced: true
|
|
2264
|
-
},
|
|
2265
|
-
"realtime.strategy": {
|
|
2266
|
-
label: "Realtime Strategy",
|
|
2267
|
-
help: "Legacy realtime alias setting. Use mode=agent or mode=bidi for new Meet joins."
|
|
2268
|
-
},
|
|
2269
|
-
"realtime.provider": {
|
|
2270
|
-
label: "Speech Provider",
|
|
2271
|
-
help: "Compatibility fallback for both realtime transcription and bidi voice. Prefer realtime.transcriptionProvider and realtime.voiceProvider for new configs."
|
|
2272
|
-
},
|
|
2273
|
-
"realtime.transcriptionProvider": {
|
|
2274
|
-
label: "Realtime Transcription Provider",
|
|
2275
|
-
help: "Agent mode uses this provider to transcribe meeting audio before regular OpenClaw TTS answers."
|
|
2276
|
-
},
|
|
2277
|
-
"realtime.voiceProvider": {
|
|
2278
|
-
label: "Bidi Voice Provider",
|
|
2279
|
-
help: "Bidi mode uses this realtime voice provider. Falls back to realtime.provider when unset."
|
|
2280
|
-
},
|
|
2281
|
-
"realtime.model": {
|
|
2282
|
-
label: "Bidi Realtime Model",
|
|
2283
|
-
help: "Only used by mode=bidi. Agent mode answers with the configured OpenClaw agent and regular TTS.",
|
|
2284
|
-
advanced: true
|
|
2285
|
-
},
|
|
2286
|
-
"realtime.instructions": {
|
|
2287
|
-
label: "Realtime Instructions",
|
|
2288
|
-
advanced: true
|
|
2289
|
-
},
|
|
2290
|
-
"realtime.introMessage": {
|
|
2291
|
-
label: "Realtime Intro Message",
|
|
2292
|
-
help: "Spoken once when the realtime bridge is ready. Set to an empty string to join silently."
|
|
2293
|
-
},
|
|
2294
|
-
"realtime.agentId": {
|
|
2295
|
-
label: "Realtime Consult Agent",
|
|
2296
|
-
help: "OpenClaw agent id used by openclaw_agent_consult. Defaults to \"main\".",
|
|
2297
|
-
advanced: true
|
|
2298
|
-
},
|
|
2299
|
-
"realtime.toolPolicy": {
|
|
2300
|
-
label: "Realtime Tool Policy",
|
|
2301
|
-
help: "Safe read-only tools are available by default; owner requests can unlock broader tools.",
|
|
2302
|
-
advanced: true
|
|
2303
|
-
},
|
|
2304
|
-
"oauth.clientId": { label: "OAuth Client ID" },
|
|
2305
|
-
"oauth.clientSecret": {
|
|
2306
|
-
label: "OAuth Client Secret",
|
|
2307
|
-
sensitive: true
|
|
2308
|
-
},
|
|
2309
|
-
"oauth.refreshToken": {
|
|
2310
|
-
label: "OAuth Refresh Token",
|
|
2311
|
-
sensitive: true
|
|
2312
|
-
},
|
|
2313
|
-
"oauth.accessToken": {
|
|
2314
|
-
label: "Cached Access Token",
|
|
2315
|
-
sensitive: true,
|
|
2316
|
-
advanced: true
|
|
2317
|
-
},
|
|
2318
|
-
"oauth.expiresAt": {
|
|
2319
|
-
label: "Cached Access Token Expiry",
|
|
2320
|
-
help: "Unix epoch milliseconds used only for the cached access-token fast path.",
|
|
2321
|
-
advanced: true
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
};
|
|
11
|
+
const googleMeetConfigSchema = { parse(value) {
|
|
12
|
+
return resolveGoogleMeetConfig(value);
|
|
13
|
+
} };
|
|
2325
14
|
const GoogleMeetToolSchema = Type.Object({
|
|
2326
15
|
action: Type.String({
|
|
2327
16
|
enum: [
|
|
@@ -2420,247 +109,186 @@ var google_meet_default = definePluginEntry({
|
|
|
2420
109
|
api,
|
|
2421
110
|
config
|
|
2422
111
|
});
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
api.registerGatewayMethod("googlemeet.join", async ({ params, client, respond }) => {
|
|
2430
|
-
try {
|
|
2431
|
-
const trustedParams = keepTrustedToolAgentId(asParamRecord(params), client);
|
|
2432
|
-
respond(true, await (await ensureRuntime()).join({
|
|
2433
|
-
url: resolveMeetingInput(config, trustedParams.url),
|
|
2434
|
-
transport: normalizeTransport(trustedParams.transport),
|
|
2435
|
-
mode: normalizeMode(trustedParams.mode),
|
|
2436
|
-
dialInNumber: normalizeOptionalString(trustedParams.dialInNumber),
|
|
2437
|
-
pin: normalizeOptionalString(trustedParams.pin),
|
|
2438
|
-
dtmfSequence: normalizeOptionalString(trustedParams.dtmfSequence),
|
|
2439
|
-
message: normalizeOptionalString(trustedParams.message),
|
|
2440
|
-
requesterSessionKey: normalizeOptionalString(trustedParams.requesterSessionKey),
|
|
2441
|
-
agentId: normalizeOptionalString(trustedParams.agentId)
|
|
2442
|
-
}));
|
|
2443
|
-
} catch (err) {
|
|
2444
|
-
sendGoogleMeetGatewayError(respond, err);
|
|
2445
|
-
}
|
|
2446
|
-
});
|
|
2447
|
-
api.registerGatewayMethod("googlemeet.create", async ({ params, client, respond }) => {
|
|
2448
|
-
try {
|
|
2449
|
-
const raw = keepTrustedToolAgentId(asParamRecord(params), client);
|
|
2450
|
-
respond(true, shouldJoinCreatedMeet(raw) ? await createAndJoinMeetFromParams({
|
|
2451
|
-
config,
|
|
2452
|
-
runtime: api.runtime,
|
|
2453
|
-
raw,
|
|
2454
|
-
ensureRuntime
|
|
2455
|
-
}) : await createMeetFromParams({
|
|
2456
|
-
config,
|
|
2457
|
-
runtime: api.runtime,
|
|
2458
|
-
raw
|
|
2459
|
-
}));
|
|
2460
|
-
} catch (err) {
|
|
2461
|
-
sendGoogleMeetGatewayError(respond, err);
|
|
2462
|
-
}
|
|
2463
|
-
});
|
|
2464
|
-
api.registerGatewayMethod("googlemeet.status", async ({ params, respond }) => {
|
|
2465
|
-
try {
|
|
2466
|
-
respond(true, await (await ensureRuntime()).status(normalizeOptionalString(params?.sessionId)));
|
|
2467
|
-
} catch (err) {
|
|
2468
|
-
sendGoogleMeetGatewayError(respond, err);
|
|
2469
|
-
}
|
|
2470
|
-
});
|
|
2471
|
-
api.registerGatewayMethod("googlemeet.transcript", async ({ params, respond }) => {
|
|
2472
|
-
try {
|
|
2473
|
-
const sessionId = normalizeOptionalString(params?.sessionId);
|
|
2474
|
-
if (!sessionId) {
|
|
2475
|
-
sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sessionId required"), ErrorCodes.INVALID_REQUEST);
|
|
2476
|
-
return;
|
|
2477
|
-
}
|
|
2478
|
-
const sinceIndex = params?.sinceIndex;
|
|
2479
|
-
if (sinceIndex !== void 0 && (typeof sinceIndex !== "number" || !Number.isSafeInteger(sinceIndex) || sinceIndex < 0)) {
|
|
2480
|
-
sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sinceIndex must be a non-negative safe integer"), ErrorCodes.INVALID_REQUEST);
|
|
2481
|
-
return;
|
|
112
|
+
const registerGatewayMethod = (method, handler) => {
|
|
113
|
+
api.registerGatewayMethod(method, async (options) => {
|
|
114
|
+
try {
|
|
115
|
+
await handler(options);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
sendGoogleMeetGatewayError(options.respond, err);
|
|
2482
118
|
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
}));
|
|
2505
|
-
} catch (err) {
|
|
2506
|
-
sendGoogleMeetGatewayError(respond, err);
|
|
2507
|
-
}
|
|
2508
|
-
});
|
|
2509
|
-
api.registerGatewayMethod("googlemeet.latest", async ({ params, respond }) => {
|
|
2510
|
-
try {
|
|
2511
|
-
const raw = asParamRecord(params);
|
|
2512
|
-
const token = await resolveGoogleMeetTokenFromParams(config, raw);
|
|
2513
|
-
const resolved = await resolveMeetingFromParams({
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
const resolveTrustedJoinParams = ({ params, client }) => {
|
|
122
|
+
const trustedParams = keepTrustedToolAgentId(asParamRecord(params), client);
|
|
123
|
+
return {
|
|
124
|
+
url: resolveMeetingInput(config, trustedParams.url),
|
|
125
|
+
transport: normalizeTransport(trustedParams.transport),
|
|
126
|
+
mode: normalizeMode(trustedParams.mode),
|
|
127
|
+
dialInNumber: normalizeOptionalString(trustedParams.dialInNumber),
|
|
128
|
+
pin: normalizeOptionalString(trustedParams.pin),
|
|
129
|
+
dtmfSequence: normalizeOptionalString(trustedParams.dtmfSequence),
|
|
130
|
+
message: normalizeOptionalString(trustedParams.message),
|
|
131
|
+
requesterSessionKey: normalizeOptionalString(trustedParams.requesterSessionKey),
|
|
132
|
+
agentId: normalizeOptionalString(trustedParams.agentId)
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
const queryActions = {
|
|
136
|
+
latest: async (raw) => {
|
|
137
|
+
const helpers = await loadGoogleMeetPluginHelpers();
|
|
138
|
+
const token = await helpers.resolveGoogleMeetTokenFromParams(config, raw);
|
|
139
|
+
const resolved = await helpers.resolveMeetingFromParams({
|
|
2514
140
|
config,
|
|
2515
141
|
raw,
|
|
2516
142
|
accessToken: token.accessToken
|
|
2517
143
|
});
|
|
2518
|
-
|
|
2519
|
-
...await fetchLatestGoogleMeetConferenceRecord({
|
|
144
|
+
return {
|
|
145
|
+
...await helpers.fetchLatestGoogleMeetConferenceRecord({
|
|
2520
146
|
accessToken: token.accessToken,
|
|
2521
147
|
meeting: resolved.meeting
|
|
2522
148
|
}),
|
|
2523
149
|
...resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {}
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
const raw = asParamRecord(params);
|
|
2532
|
-
const token = await resolveGoogleMeetTokenFromParams(config, raw);
|
|
2533
|
-
const window = raw.today === true ? buildGoogleMeetCalendarDayWindow() : {};
|
|
2534
|
-
respond(true, await listGoogleMeetCalendarEvents({
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
calendar_events: async (raw) => {
|
|
153
|
+
const helpers = await loadGoogleMeetPluginHelpers();
|
|
154
|
+
const token = await helpers.resolveGoogleMeetTokenFromParams(config, raw);
|
|
155
|
+
const window = raw.today === true ? helpers.buildGoogleMeetCalendarDayWindow() : {};
|
|
156
|
+
return helpers.listGoogleMeetCalendarEvents({
|
|
2535
157
|
accessToken: token.accessToken,
|
|
2536
158
|
calendarId: normalizeOptionalString(raw.calendarId),
|
|
2537
159
|
eventQuery: normalizeOptionalString(raw.event),
|
|
2538
160
|
...window
|
|
2539
|
-
})
|
|
2540
|
-
}
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
const
|
|
2547
|
-
|
|
2548
|
-
respond(true, await fetchGoogleMeetArtifacts({
|
|
2549
|
-
accessToken: resolved.token.accessToken,
|
|
2550
|
-
meeting: resolved.meeting,
|
|
2551
|
-
conferenceRecord: resolved.conferenceRecord,
|
|
2552
|
-
pageSize: resolved.pageSize,
|
|
2553
|
-
includeTranscriptEntries: resolved.includeTranscriptEntries,
|
|
2554
|
-
includeDocumentBodies: resolved.includeDocumentBodies,
|
|
2555
|
-
allConferenceRecords: resolved.allConferenceRecords
|
|
2556
|
-
}));
|
|
2557
|
-
} catch (err) {
|
|
2558
|
-
sendGoogleMeetGatewayError(respond, err);
|
|
2559
|
-
}
|
|
2560
|
-
});
|
|
2561
|
-
api.registerGatewayMethod("googlemeet.attendance", async ({ params, respond }) => {
|
|
2562
|
-
try {
|
|
2563
|
-
const raw = asParamRecord(params);
|
|
2564
|
-
const resolved = await resolveArtifactQueryFromParams(config, raw);
|
|
2565
|
-
respond(true, await fetchGoogleMeetAttendance({
|
|
2566
|
-
accessToken: resolved.token.accessToken,
|
|
2567
|
-
meeting: resolved.meeting,
|
|
2568
|
-
conferenceRecord: resolved.conferenceRecord,
|
|
2569
|
-
pageSize: resolved.pageSize,
|
|
2570
|
-
allConferenceRecords: resolved.allConferenceRecords,
|
|
2571
|
-
mergeDuplicateParticipants: resolved.mergeDuplicateParticipants,
|
|
2572
|
-
lateAfterMinutes: resolved.lateAfterMinutes,
|
|
2573
|
-
earlyBeforeMinutes: resolved.earlyBeforeMinutes
|
|
2574
|
-
}));
|
|
2575
|
-
} catch (err) {
|
|
2576
|
-
sendGoogleMeetGatewayError(respond, err);
|
|
2577
|
-
}
|
|
2578
|
-
});
|
|
2579
|
-
api.registerGatewayMethod("googlemeet.export", async ({ params, respond }) => {
|
|
2580
|
-
try {
|
|
2581
|
-
respond(true, await exportGoogleMeetBundleFromParams(config, asParamRecord(params)));
|
|
2582
|
-
} catch (err) {
|
|
2583
|
-
sendGoogleMeetGatewayError(respond, err);
|
|
2584
|
-
}
|
|
2585
|
-
});
|
|
2586
|
-
api.registerGatewayMethod("googlemeet.leave", async ({ params, respond }) => {
|
|
2587
|
-
try {
|
|
2588
|
-
const sessionId = normalizeOptionalString(params?.sessionId);
|
|
2589
|
-
if (!sessionId) {
|
|
2590
|
-
sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sessionId required"), ErrorCodes.INVALID_REQUEST);
|
|
2591
|
-
return;
|
|
2592
|
-
}
|
|
2593
|
-
respond(true, await (await ensureRuntime()).leave(sessionId));
|
|
2594
|
-
} catch (err) {
|
|
2595
|
-
sendGoogleMeetGatewayError(respond, err);
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
artifacts: async (raw) => {
|
|
164
|
+
const helpers = await loadGoogleMeetPluginHelpers();
|
|
165
|
+
return helpers.fetchResolvedGoogleMeetArtifacts(await helpers.resolveArtifactQueryFromParams(config, raw));
|
|
166
|
+
},
|
|
167
|
+
attendance: async (raw) => {
|
|
168
|
+
const helpers = await loadGoogleMeetPluginHelpers();
|
|
169
|
+
return helpers.fetchResolvedGoogleMeetAttendance(await helpers.resolveArtifactQueryFromParams(config, raw));
|
|
2596
170
|
}
|
|
2597
|
-
}
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
171
|
+
};
|
|
172
|
+
const transcriptSourceRuntime = async () => (await ensureRuntime()).transcriptSourceRuntime();
|
|
173
|
+
api.registerTranscriptSourceProvider({
|
|
174
|
+
id: "google-meet",
|
|
175
|
+
aliases: ["googlemeet", "meet"],
|
|
176
|
+
name: "Google Meet",
|
|
177
|
+
sourceKinds: ["live-caption"],
|
|
178
|
+
start: async (request) => await (await transcriptSourceRuntime()).startTranscriptSource(request),
|
|
179
|
+
stop: async (request) => await (await transcriptSourceRuntime()).stopTranscriptSource(request)
|
|
180
|
+
});
|
|
181
|
+
registerGatewayMethod("googlemeet.join", async (options) => {
|
|
182
|
+
const runtime = await ensureRuntime();
|
|
183
|
+
options.respond(true, await runtime.join(resolveTrustedJoinParams(options)));
|
|
184
|
+
});
|
|
185
|
+
registerGatewayMethod("googlemeet.create", async ({ params, client, respond }) => {
|
|
186
|
+
const raw = keepTrustedToolAgentId(asParamRecord(params), client);
|
|
187
|
+
const helpers = await loadGoogleMeetPluginHelpers();
|
|
188
|
+
respond(true, shouldJoinCreatedMeet(raw) ? await helpers.createAndJoinMeetFromParams({
|
|
189
|
+
config,
|
|
190
|
+
runtime: api.runtime,
|
|
191
|
+
raw,
|
|
192
|
+
ensureRuntime
|
|
193
|
+
}) : await helpers.createMeetFromParams({
|
|
194
|
+
config,
|
|
195
|
+
runtime: api.runtime,
|
|
196
|
+
raw
|
|
197
|
+
}));
|
|
198
|
+
});
|
|
199
|
+
registerGatewayMethod("googlemeet.status", async ({ params, respond }) => {
|
|
200
|
+
respond(true, await (await ensureRuntime()).status(normalizeOptionalString(params?.sessionId)));
|
|
201
|
+
});
|
|
202
|
+
registerGatewayMethod("googlemeet.transcript", async ({ params, respond }) => {
|
|
203
|
+
const sessionId = normalizeOptionalString(params?.sessionId);
|
|
204
|
+
if (!sessionId) {
|
|
205
|
+
sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sessionId required"), "INVALID_REQUEST");
|
|
206
|
+
return;
|
|
2607
207
|
}
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
if (!sessionId) {
|
|
2613
|
-
sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sessionId required"), ErrorCodes.INVALID_REQUEST);
|
|
2614
|
-
return;
|
|
2615
|
-
}
|
|
2616
|
-
respond(true, await (await ensureRuntime()).speak(sessionId, normalizeOptionalString(params?.message)));
|
|
2617
|
-
} catch (err) {
|
|
2618
|
-
sendGoogleMeetGatewayError(respond, err);
|
|
208
|
+
const sinceIndex = params?.sinceIndex;
|
|
209
|
+
if (sinceIndex !== void 0 && (typeof sinceIndex !== "number" || !Number.isSafeInteger(sinceIndex) || sinceIndex < 0)) {
|
|
210
|
+
sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sinceIndex must be a non-negative safe integer"), "INVALID_REQUEST");
|
|
211
|
+
return;
|
|
2619
212
|
}
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
213
|
+
respond(true, await (await ensureRuntime()).transcript(sessionId, sinceIndex === void 0 ? {} : { sinceIndex }));
|
|
214
|
+
});
|
|
215
|
+
registerGatewayMethod("googlemeet.recoverCurrentTab", async ({ params, respond }) => {
|
|
216
|
+
respond(true, await (await ensureRuntime()).recoverCurrentTab({
|
|
217
|
+
url: normalizeOptionalString(params?.url),
|
|
218
|
+
transport: normalizeTransport(params?.transport)
|
|
219
|
+
}));
|
|
220
|
+
});
|
|
221
|
+
registerGatewayMethod("googlemeet.setup", async ({ params, respond }) => {
|
|
222
|
+
respond(true, await (await ensureRuntime()).setupStatus({
|
|
223
|
+
transport: normalizeTransport(params?.transport),
|
|
224
|
+
mode: normalizeMode(params?.mode),
|
|
225
|
+
dialInNumber: normalizeOptionalString(params?.dialInNumber)
|
|
226
|
+
}));
|
|
227
|
+
});
|
|
228
|
+
for (const [method, action] of [
|
|
229
|
+
["googlemeet.latest", "latest"],
|
|
230
|
+
["googlemeet.calendarEvents", "calendar_events"],
|
|
231
|
+
["googlemeet.artifacts", "artifacts"],
|
|
232
|
+
["googlemeet.attendance", "attendance"]
|
|
233
|
+
]) registerGatewayMethod(method, async ({ params, respond }) => {
|
|
234
|
+
respond(true, await queryActions[action](asParamRecord(params)));
|
|
235
|
+
});
|
|
236
|
+
registerGatewayMethod("googlemeet.export", async ({ params, respond }) => {
|
|
237
|
+
respond(true, await (await loadGoogleMeetPluginHelpers()).exportGoogleMeetBundleFromParams(config, asParamRecord(params)));
|
|
238
|
+
});
|
|
239
|
+
registerGatewayMethod("googlemeet.leave", async ({ params, respond }) => {
|
|
240
|
+
const sessionId = normalizeOptionalString(params?.sessionId);
|
|
241
|
+
if (!sessionId) {
|
|
242
|
+
sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sessionId required"), "INVALID_REQUEST");
|
|
243
|
+
return;
|
|
2637
244
|
}
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
245
|
+
respond(true, await (await ensureRuntime()).leave(sessionId));
|
|
246
|
+
});
|
|
247
|
+
registerGatewayMethod("googlemeet.endActiveConference", async ({ params, respond }) => {
|
|
248
|
+
const raw = asParamRecord(params);
|
|
249
|
+
const helpers = await loadGoogleMeetPluginHelpers();
|
|
250
|
+
const token = await helpers.resolveGoogleMeetTokenFromParams(config, raw);
|
|
251
|
+
respond(true, await helpers.endGoogleMeetActiveConference({
|
|
252
|
+
accessToken: token.accessToken,
|
|
253
|
+
meeting: resolveMeetingInput(config, raw.meeting)
|
|
254
|
+
}));
|
|
255
|
+
});
|
|
256
|
+
registerGatewayMethod("googlemeet.speak", async ({ params, respond }) => {
|
|
257
|
+
const sessionId = normalizeOptionalString(params?.sessionId);
|
|
258
|
+
if (!sessionId) {
|
|
259
|
+
sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sessionId required"), "INVALID_REQUEST");
|
|
260
|
+
return;
|
|
2651
261
|
}
|
|
262
|
+
respond(true, await (await ensureRuntime()).speak(sessionId, normalizeOptionalString(params?.message)));
|
|
263
|
+
});
|
|
264
|
+
registerGatewayMethod("googlemeet.testSpeech", async (options) => {
|
|
265
|
+
const runtime = await ensureRuntime();
|
|
266
|
+
options.respond(true, await runtime.testSpeech(resolveTrustedJoinParams(options)));
|
|
267
|
+
});
|
|
268
|
+
registerGatewayMethod("googlemeet.testListen", async ({ params, client, respond }) => {
|
|
269
|
+
const trustedParams = keepTrustedToolAgentId(asParamRecord(params), client);
|
|
270
|
+
const runtime = await ensureRuntime();
|
|
271
|
+
const { readPositiveIntegerParam } = await import("openclaw/plugin-sdk/param-readers");
|
|
272
|
+
respond(true, await runtime.testListen({
|
|
273
|
+
url: resolveMeetingInput(config, trustedParams.url),
|
|
274
|
+
transport: normalizeTransport(trustedParams.transport),
|
|
275
|
+
mode: normalizeMode(trustedParams.mode),
|
|
276
|
+
agentId: normalizeOptionalString(trustedParams.agentId),
|
|
277
|
+
timeoutMs: readPositiveIntegerParam(trustedParams, "timeoutMs")
|
|
278
|
+
}));
|
|
2652
279
|
});
|
|
2653
280
|
api.registerTool((toolContext) => ({
|
|
2654
281
|
name: "google_meet",
|
|
2655
282
|
label: "Google Meet",
|
|
2656
|
-
description: "Join and track Google Meet sessions through Chrome or Twilio. Call setup_status before join/create/test_listen/test_speech; if it reports a Chrome node offline, local audio missing, or missing Twilio dial plan, surface that blocker instead of retrying or switching transports. Twilio cannot dial a Meet URL directly: provide dialInNumber plus optional pin/dtmfSequence, or configure twilio.defaultDialInNumber. Offline nodes are diagnostics only, not usable candidates.
|
|
283
|
+
description: "Join and track Google Meet sessions through Chrome or Twilio. Call setup_status before join/create/test_listen/test_speech; if it reports a Chrome node offline, local audio missing, or missing Twilio dial plan, surface that blocker instead of retrying or switching transports. Twilio cannot dial a Meet URL directly: provide dialInNumber plus optional pin/dtmfSequence, or configure twilio.defaultDialInNumber. Offline nodes are diagnostics only, not usable candidates. Local Chrome talk-back needs macOS with BlackHole 2ch or Linux with PipeWire-Pulse; otherwise use mode=transcribe, transport=twilio, or a supported chrome-node. If a Meet tab is already open after a timeout, call recover_current_tab before retrying join to report login, permission, or admission blockers without opening another tab.",
|
|
2657
284
|
parameters: GoogleMeetToolSchema,
|
|
2658
285
|
async execute(_toolCallId, params) {
|
|
2659
286
|
const raw = asParamRecord(params);
|
|
2660
287
|
const requesterSessionKey = normalizeOptionalString(toolContext.sessionKey);
|
|
2661
|
-
const contextAgentId = toolContext.agentId ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
|
|
2662
|
-
const agentId = contextAgentId ? normalizeAgentId(contextAgentId) : void 0;
|
|
2663
288
|
try {
|
|
289
|
+
const { normalizeAgentId, parseAgentSessionKey } = await import("openclaw/plugin-sdk/routing");
|
|
290
|
+
const contextAgentId = toolContext.agentId ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
|
|
291
|
+
const agentId = contextAgentId ? normalizeAgentId(contextAgentId) : void 0;
|
|
2664
292
|
const needsTrustedAgentRouting = Boolean(agentId && agentId !== "main");
|
|
2665
293
|
const useTrustedRuntime = needsTrustedAgentRouting ? await api.runtime.gateway.isAvailable() : false;
|
|
2666
294
|
if (needsTrustedAgentRouting && !useTrustedRuntime) throw new Error("Per-agent Google Meet routing requires a Gateway-hosted agent run.");
|
|
@@ -2674,131 +302,49 @@ var google_meet_default = definePluginEntry({
|
|
|
2674
302
|
raw
|
|
2675
303
|
});
|
|
2676
304
|
switch (raw.action) {
|
|
2677
|
-
case "join":
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
raw: rawWithRequester,
|
|
2681
|
-
runtime: useTrustedRuntime ? api.runtime : void 0
|
|
2682
|
-
}));
|
|
2683
|
-
case "create": return jsonResult(await callGoogleMeetGatewayFromTool({
|
|
2684
|
-
config,
|
|
2685
|
-
action: "create",
|
|
2686
|
-
raw: rawWithRequester,
|
|
2687
|
-
runtime: useTrustedRuntime ? api.runtime : void 0
|
|
2688
|
-
}));
|
|
2689
|
-
case "test_speech": return jsonResult(await callGoogleMeetGatewayFromTool({
|
|
2690
|
-
config,
|
|
2691
|
-
action: "test_speech",
|
|
2692
|
-
raw: rawWithRequester,
|
|
2693
|
-
runtime: useTrustedRuntime ? api.runtime : void 0
|
|
2694
|
-
}));
|
|
305
|
+
case "join":
|
|
306
|
+
case "create":
|
|
307
|
+
case "test_speech":
|
|
2695
308
|
case "test_listen": return jsonResult(await callGoogleMeetGatewayFromTool({
|
|
2696
309
|
config,
|
|
2697
|
-
action:
|
|
310
|
+
action: raw.action,
|
|
2698
311
|
raw: rawWithRequester,
|
|
2699
312
|
runtime: useTrustedRuntime ? api.runtime : void 0
|
|
2700
313
|
}));
|
|
2701
|
-
case "status":
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
case "transcript": return jsonResult(await callGoogleMeetGatewayFromTool({
|
|
2707
|
-
config,
|
|
2708
|
-
action: "transcript",
|
|
2709
|
-
raw
|
|
2710
|
-
}));
|
|
2711
|
-
case "recover_current_tab": return jsonResult(await callGoogleMeetGatewayFromTool({
|
|
2712
|
-
config,
|
|
2713
|
-
action: "recover_current_tab",
|
|
2714
|
-
raw
|
|
2715
|
-
}));
|
|
2716
|
-
case "setup_status": return jsonResult(await callGoogleMeetGatewayFromTool({
|
|
314
|
+
case "status":
|
|
315
|
+
case "transcript":
|
|
316
|
+
case "recover_current_tab":
|
|
317
|
+
case "setup_status":
|
|
318
|
+
case "end_active_conference": return jsonResult(await callGoogleMeetGatewayFromTool({
|
|
2717
319
|
config,
|
|
2718
|
-
action:
|
|
320
|
+
action: raw.action,
|
|
2719
321
|
raw
|
|
2720
322
|
}));
|
|
2721
323
|
case "resolve_space": {
|
|
2722
|
-
const { token: _token, ...result } = await resolveSpaceFromParams(config, raw);
|
|
324
|
+
const { token: _token, ...result } = await (await loadGoogleMeetPluginHelpers()).resolveSpaceFromParams(config, raw);
|
|
2723
325
|
return jsonResult(result);
|
|
2724
326
|
}
|
|
2725
327
|
case "preflight": {
|
|
2726
|
-
const
|
|
2727
|
-
|
|
328
|
+
const helpers = await loadGoogleMeetPluginHelpers();
|
|
329
|
+
const { meeting, token, space } = await helpers.resolveSpaceFromParams(config, raw);
|
|
330
|
+
return jsonResult(helpers.buildGoogleMeetPreflightReport({
|
|
2728
331
|
input: meeting,
|
|
2729
332
|
space,
|
|
2730
333
|
previewAcknowledged: config.preview.enrollmentAcknowledged,
|
|
2731
334
|
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token"
|
|
2732
335
|
}));
|
|
2733
336
|
}
|
|
2734
|
-
case "latest":
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
accessToken: token.accessToken
|
|
2740
|
-
});
|
|
2741
|
-
return jsonResult({
|
|
2742
|
-
...await fetchLatestGoogleMeetConferenceRecord({
|
|
2743
|
-
accessToken: token.accessToken,
|
|
2744
|
-
meeting: resolved.meeting
|
|
2745
|
-
}),
|
|
2746
|
-
...resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {}
|
|
2747
|
-
});
|
|
2748
|
-
}
|
|
2749
|
-
case "calendar_events": {
|
|
2750
|
-
const token = await resolveGoogleMeetTokenFromParams(config, raw);
|
|
2751
|
-
const window = raw.today === true ? buildGoogleMeetCalendarDayWindow() : {};
|
|
2752
|
-
return jsonResult(await listGoogleMeetCalendarEvents({
|
|
2753
|
-
accessToken: token.accessToken,
|
|
2754
|
-
calendarId: normalizeOptionalString(raw.calendarId),
|
|
2755
|
-
eventQuery: normalizeOptionalString(raw.event),
|
|
2756
|
-
...window
|
|
2757
|
-
}));
|
|
2758
|
-
}
|
|
2759
|
-
case "artifacts": {
|
|
2760
|
-
const resolved = await resolveArtifactQueryFromParams(config, raw);
|
|
2761
|
-
return jsonResult(await fetchGoogleMeetArtifacts({
|
|
2762
|
-
accessToken: resolved.token.accessToken,
|
|
2763
|
-
meeting: resolved.meeting,
|
|
2764
|
-
conferenceRecord: resolved.conferenceRecord,
|
|
2765
|
-
pageSize: resolved.pageSize,
|
|
2766
|
-
includeTranscriptEntries: resolved.includeTranscriptEntries,
|
|
2767
|
-
includeDocumentBodies: resolved.includeDocumentBodies,
|
|
2768
|
-
allConferenceRecords: resolved.allConferenceRecords
|
|
2769
|
-
}));
|
|
2770
|
-
}
|
|
2771
|
-
case "attendance": {
|
|
2772
|
-
const resolved = await resolveArtifactQueryFromParams(config, raw);
|
|
2773
|
-
return jsonResult(await fetchGoogleMeetAttendance({
|
|
2774
|
-
accessToken: resolved.token.accessToken,
|
|
2775
|
-
meeting: resolved.meeting,
|
|
2776
|
-
conferenceRecord: resolved.conferenceRecord,
|
|
2777
|
-
pageSize: resolved.pageSize,
|
|
2778
|
-
allConferenceRecords: resolved.allConferenceRecords,
|
|
2779
|
-
mergeDuplicateParticipants: resolved.mergeDuplicateParticipants,
|
|
2780
|
-
lateAfterMinutes: resolved.lateAfterMinutes,
|
|
2781
|
-
earlyBeforeMinutes: resolved.earlyBeforeMinutes
|
|
2782
|
-
}));
|
|
2783
|
-
}
|
|
2784
|
-
case "export": return jsonResult(await exportGoogleMeetBundleFromParams(config, raw));
|
|
337
|
+
case "latest":
|
|
338
|
+
case "calendar_events":
|
|
339
|
+
case "artifacts":
|
|
340
|
+
case "attendance": return jsonResult(await queryActions[raw.action](raw));
|
|
341
|
+
case "export": return jsonResult(await (await loadGoogleMeetPluginHelpers()).exportGoogleMeetBundleFromParams(config, raw));
|
|
2785
342
|
case "leave":
|
|
2786
|
-
if (!normalizeOptionalString(raw.sessionId)) throw new Error("sessionId required");
|
|
2787
|
-
return jsonResult(await callGoogleMeetGatewayFromTool({
|
|
2788
|
-
config,
|
|
2789
|
-
action: "leave",
|
|
2790
|
-
raw
|
|
2791
|
-
}));
|
|
2792
|
-
case "end_active_conference": return jsonResult(await callGoogleMeetGatewayFromTool({
|
|
2793
|
-
config,
|
|
2794
|
-
action: "end_active_conference",
|
|
2795
|
-
raw
|
|
2796
|
-
}));
|
|
2797
343
|
case "speak":
|
|
2798
344
|
if (!normalizeOptionalString(raw.sessionId)) throw new Error("sessionId required");
|
|
2799
345
|
return jsonResult(await callGoogleMeetGatewayFromTool({
|
|
2800
346
|
config,
|
|
2801
|
-
action:
|
|
347
|
+
action: raw.action,
|
|
2802
348
|
raw
|
|
2803
349
|
}));
|
|
2804
350
|
default: throw new Error("unknown google_meet action");
|
|
@@ -2809,12 +355,12 @@ var google_meet_default = definePluginEntry({
|
|
|
2809
355
|
}
|
|
2810
356
|
}), { name: "google_meet" });
|
|
2811
357
|
api.registerNodeHostCommand({
|
|
2812
|
-
command:
|
|
358
|
+
command: GOOGLE_MEET_NODE_COMMAND,
|
|
2813
359
|
cap: "google-meet",
|
|
2814
360
|
dangerous: true,
|
|
2815
|
-
handle: handleGoogleMeetNodeHostCommand
|
|
361
|
+
handle: async (paramsJSON) => await (await loadGoogleMeetNodeHostModule()).handleGoogleMeetNodeHostCommand(paramsJSON)
|
|
2816
362
|
});
|
|
2817
|
-
api.registerNodeInvokePolicy(
|
|
363
|
+
api.registerNodeInvokePolicy(createLazyGoogleMeetNodeInvokePolicy(config));
|
|
2818
364
|
api.registerCli(async ({ program }) => {
|
|
2819
365
|
const { registerGoogleMeetCli } = await loadGoogleMeetCliModule();
|
|
2820
366
|
registerGoogleMeetCli({
|
|
@@ -2829,4 +375,4 @@ var google_meet_default = definePluginEntry({
|
|
|
2829
375
|
}
|
|
2830
376
|
});
|
|
2831
377
|
//#endregion
|
|
2832
|
-
export {
|
|
378
|
+
export { google_meet_default as default, testing };
|