@openclaw/google-meet 2026.5.3-beta.2 → 2026.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{cli-CSyT9_N6.js → cli-B_wJa8XB.js} +6 -0
- package/dist/index.js +219 -86
- package/package.json +4 -4
|
@@ -143,6 +143,12 @@ function writeDoctorStatus(status) {
|
|
|
143
143
|
writeStdoutLine("bridge closed: %s", formatBoolean(health?.bridgeClosed));
|
|
144
144
|
writeStdoutLine("browser url: %s", formatOptional(health?.browserUrl));
|
|
145
145
|
if (health?.lastCaptionText) writeStdoutLine("last caption text: %s%s", health.lastCaptionSpeaker ? `${health.lastCaptionSpeaker}: ` : "", health.lastCaptionText);
|
|
146
|
+
writeStdoutLine("realtime transcript lines: %s", health?.realtimeTranscriptLines ?? 0);
|
|
147
|
+
if (health?.lastRealtimeTranscriptText) writeStdoutLine("last realtime transcript: %s%s", health.lastRealtimeTranscriptRole ? `${health.lastRealtimeTranscriptRole}: ` : "", health.lastRealtimeTranscriptText);
|
|
148
|
+
if (health?.lastRealtimeEventType) {
|
|
149
|
+
const detail = health.lastRealtimeEventDetail ? ` ${health.lastRealtimeEventDetail}` : "";
|
|
150
|
+
writeStdoutLine("last realtime event: %s%s", health.lastRealtimeEventType, detail);
|
|
151
|
+
}
|
|
146
152
|
}
|
|
147
153
|
}
|
|
148
154
|
function sanitizeOAuthErrorMessage(error) {
|
package/dist/index.js
CHANGED
|
@@ -342,6 +342,42 @@ async function consultOpenClawAgentForGoogleMeet(params) {
|
|
|
342
342
|
}
|
|
343
343
|
//#endregion
|
|
344
344
|
//#region extensions/google-meet/src/realtime.ts
|
|
345
|
+
function recordGoogleMeetRealtimeTranscript(transcript, role, text) {
|
|
346
|
+
const entry = {
|
|
347
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
348
|
+
role,
|
|
349
|
+
text
|
|
350
|
+
};
|
|
351
|
+
transcript.push(entry);
|
|
352
|
+
if (transcript.length > 40) transcript.splice(0, transcript.length - 40);
|
|
353
|
+
return entry;
|
|
354
|
+
}
|
|
355
|
+
function getGoogleMeetRealtimeTranscriptHealth(transcript) {
|
|
356
|
+
const last = transcript.at(-1);
|
|
357
|
+
return {
|
|
358
|
+
realtimeTranscriptLines: transcript.length,
|
|
359
|
+
lastRealtimeTranscriptAt: last?.at,
|
|
360
|
+
lastRealtimeTranscriptRole: last?.role,
|
|
361
|
+
lastRealtimeTranscriptText: last?.text,
|
|
362
|
+
recentRealtimeTranscript: transcript.slice(-5)
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function recordGoogleMeetRealtimeEvent(events, event) {
|
|
366
|
+
events.push({
|
|
367
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
368
|
+
...event
|
|
369
|
+
});
|
|
370
|
+
if (events.length > 40) events.splice(0, events.length - 40);
|
|
371
|
+
}
|
|
372
|
+
function getGoogleMeetRealtimeEventHealth(events) {
|
|
373
|
+
const last = events.at(-1);
|
|
374
|
+
return {
|
|
375
|
+
lastRealtimeEventAt: last?.at,
|
|
376
|
+
lastRealtimeEventType: last ? `${last.direction}:${last.type}` : void 0,
|
|
377
|
+
lastRealtimeEventDetail: last?.detail,
|
|
378
|
+
recentRealtimeEvents: events.slice(-10)
|
|
379
|
+
};
|
|
380
|
+
}
|
|
345
381
|
function splitCommand$1(argv) {
|
|
346
382
|
const [command, ...args] = argv;
|
|
347
383
|
if (!command) throw new Error("audio bridge command must not be empty");
|
|
@@ -528,6 +564,7 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
528
564
|
providers: params.providers
|
|
529
565
|
});
|
|
530
566
|
const transcript = [];
|
|
567
|
+
const realtimeEvents = [];
|
|
531
568
|
bridge = createRealtimeVoiceBridgeSession({
|
|
532
569
|
provider: resolved.provider,
|
|
533
570
|
providerConfig: resolved.providerConfig,
|
|
@@ -550,12 +587,15 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
550
587
|
},
|
|
551
588
|
onTranscript: (role, text, isFinal) => {
|
|
552
589
|
if (isFinal) {
|
|
553
|
-
transcript
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
590
|
+
recordGoogleMeetRealtimeTranscript(transcript, role, text);
|
|
591
|
+
params.logger.info(`[google-meet] realtime ${role}: ${text}`);
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
onEvent: (event) => {
|
|
595
|
+
recordGoogleMeetRealtimeEvent(realtimeEvents, event);
|
|
596
|
+
if (event.type === "error" || event.type === "response.done") {
|
|
597
|
+
const detail = event.detail ? ` ${event.detail}` : "";
|
|
598
|
+
params.logger.info(`[google-meet] realtime ${event.direction}:${event.type}${detail}`);
|
|
559
599
|
}
|
|
560
600
|
},
|
|
561
601
|
onToolCall: (event, session) => {
|
|
@@ -620,6 +660,8 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
620
660
|
lastInputBytes,
|
|
621
661
|
lastOutputBytes,
|
|
622
662
|
suppressedInputBytes,
|
|
663
|
+
...getGoogleMeetRealtimeTranscriptHealth(transcript),
|
|
664
|
+
...getGoogleMeetRealtimeEventHealth(realtimeEvents),
|
|
623
665
|
lastClearAt,
|
|
624
666
|
clearCount,
|
|
625
667
|
bridgeClosed: stopped
|
|
@@ -653,6 +695,7 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
653
695
|
providers: params.providers
|
|
654
696
|
});
|
|
655
697
|
const transcript = [];
|
|
698
|
+
const realtimeEvents = [];
|
|
656
699
|
const stop = async () => {
|
|
657
700
|
if (stopped) return;
|
|
658
701
|
stopped = true;
|
|
@@ -722,12 +765,15 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
722
765
|
},
|
|
723
766
|
onTranscript: (role, text, isFinal) => {
|
|
724
767
|
if (isFinal) {
|
|
725
|
-
transcript
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
768
|
+
recordGoogleMeetRealtimeTranscript(transcript, role, text);
|
|
769
|
+
params.logger.info(`[google-meet] node realtime ${role}: ${text}`);
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
onEvent: (event) => {
|
|
773
|
+
recordGoogleMeetRealtimeEvent(realtimeEvents, event);
|
|
774
|
+
if (event.type === "error" || event.type === "response.done") {
|
|
775
|
+
const detail = event.detail ? ` ${event.detail}` : "";
|
|
776
|
+
params.logger.info(`[google-meet] node realtime ${event.direction}:${event.type}${detail}`);
|
|
731
777
|
}
|
|
732
778
|
},
|
|
733
779
|
onToolCall: (event, session) => {
|
|
@@ -818,6 +864,8 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
818
864
|
lastClearAt,
|
|
819
865
|
lastInputBytes,
|
|
820
866
|
lastOutputBytes,
|
|
867
|
+
...getGoogleMeetRealtimeTranscriptHealth(transcript),
|
|
868
|
+
...getGoogleMeetRealtimeEventHealth(realtimeEvents),
|
|
821
869
|
consecutiveInputErrors,
|
|
822
870
|
lastInputError,
|
|
823
871
|
clearCount,
|
|
@@ -850,8 +898,8 @@ async function assertBlackHole2chAvailable(params) {
|
|
|
850
898
|
}
|
|
851
899
|
}
|
|
852
900
|
async function launchChromeMeet(params) {
|
|
853
|
-
|
|
854
|
-
|
|
901
|
+
const checkRealtimeAudioPrerequisites = async () => {
|
|
902
|
+
if (params.mode !== "realtime") return;
|
|
855
903
|
await assertBlackHole2chAvailable({
|
|
856
904
|
runtime: params.runtime,
|
|
857
905
|
timeoutMs: Math.min(params.config.chrome.joinTimeoutMs, 1e4)
|
|
@@ -860,50 +908,44 @@ async function launchChromeMeet(params) {
|
|
|
860
908
|
const health = await params.runtime.system.runCommandWithTimeout(params.config.chrome.audioBridgeHealthCommand, { timeoutMs: params.config.chrome.joinTimeoutMs });
|
|
861
909
|
if (health.code !== 0) throw new Error(`Chrome audio bridge health check failed: ${health.stderr || health.stdout || health.code}`);
|
|
862
910
|
}
|
|
911
|
+
};
|
|
912
|
+
const startRealtimeAudioBridge = async () => {
|
|
913
|
+
if (params.mode !== "realtime") return;
|
|
863
914
|
if (params.config.chrome.audioBridgeCommand) {
|
|
864
915
|
const bridge = await params.runtime.system.runCommandWithTimeout(params.config.chrome.audioBridgeCommand, { timeoutMs: params.config.chrome.joinTimeoutMs });
|
|
865
916
|
if (bridge.code !== 0) throw new Error(`failed to start Chrome audio bridge: ${bridge.stderr || bridge.stdout || bridge.code}`);
|
|
866
|
-
|
|
867
|
-
} else {
|
|
868
|
-
if (!params.config.chrome.audioInputCommand || !params.config.chrome.audioOutputCommand) throw new Error("Chrome realtime mode requires chrome.audioInputCommand and chrome.audioOutputCommand, or chrome.audioBridgeCommand for an external bridge.");
|
|
869
|
-
audioBridge = {
|
|
870
|
-
type: "command-pair",
|
|
871
|
-
...await startCommandRealtimeAudioBridge({
|
|
872
|
-
config: params.config,
|
|
873
|
-
fullConfig: params.fullConfig,
|
|
874
|
-
runtime: params.runtime,
|
|
875
|
-
meetingSessionId: params.meetingSessionId,
|
|
876
|
-
inputCommand: params.config.chrome.audioInputCommand,
|
|
877
|
-
outputCommand: params.config.chrome.audioOutputCommand,
|
|
878
|
-
logger: params.logger
|
|
879
|
-
})
|
|
880
|
-
};
|
|
917
|
+
return { type: "external-command" };
|
|
881
918
|
}
|
|
882
|
-
|
|
919
|
+
if (!params.config.chrome.audioInputCommand || !params.config.chrome.audioOutputCommand) throw new Error("Chrome realtime mode requires chrome.audioInputCommand and chrome.audioOutputCommand, or chrome.audioBridgeCommand for an external bridge.");
|
|
920
|
+
return {
|
|
921
|
+
type: "command-pair",
|
|
922
|
+
...await startCommandRealtimeAudioBridge({
|
|
923
|
+
config: params.config,
|
|
924
|
+
fullConfig: params.fullConfig,
|
|
925
|
+
runtime: params.runtime,
|
|
926
|
+
meetingSessionId: params.meetingSessionId,
|
|
927
|
+
inputCommand: params.config.chrome.audioInputCommand,
|
|
928
|
+
outputCommand: params.config.chrome.audioOutputCommand,
|
|
929
|
+
logger: params.logger
|
|
930
|
+
})
|
|
931
|
+
};
|
|
932
|
+
};
|
|
933
|
+
await checkRealtimeAudioPrerequisites();
|
|
883
934
|
if (!params.config.chrome.launch) return {
|
|
884
935
|
launched: false,
|
|
885
|
-
audioBridge
|
|
936
|
+
audioBridge: await startRealtimeAudioBridge()
|
|
886
937
|
};
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
938
|
+
const result = await openMeetWithBrowserRequest({
|
|
939
|
+
callBrowser: callLocalBrowserRequest,
|
|
940
|
+
config: params.config,
|
|
941
|
+
mode: params.mode,
|
|
942
|
+
url: params.url
|
|
943
|
+
});
|
|
944
|
+
const audioBridge = params.mode === "realtime" && result.browser?.inCall === true && result.browser.micMuted !== true && result.browser.manualActionRequired !== true ? await startRealtimeAudioBridge() : void 0;
|
|
945
|
+
return {
|
|
946
|
+
...result,
|
|
947
|
+
audioBridge
|
|
892
948
|
};
|
|
893
|
-
try {
|
|
894
|
-
return {
|
|
895
|
-
...await openMeetWithBrowserRequest({
|
|
896
|
-
callBrowser: callLocalBrowserRequest,
|
|
897
|
-
config: params.config,
|
|
898
|
-
mode: params.mode,
|
|
899
|
-
url: params.url
|
|
900
|
-
}),
|
|
901
|
-
audioBridge
|
|
902
|
-
};
|
|
903
|
-
} catch (error) {
|
|
904
|
-
await stopCommandPairBridge();
|
|
905
|
-
throw error;
|
|
906
|
-
}
|
|
907
949
|
}
|
|
908
950
|
function parseNodeStartResult(raw) {
|
|
909
951
|
const value = raw && typeof raw === "object" && "payload" in raw ? raw.payload : raw;
|
|
@@ -970,6 +1012,7 @@ async function grantMeetMediaPermissions(params) {
|
|
|
970
1012
|
origin: "https://meet.google.com",
|
|
971
1013
|
permissions: ["audioCapture", "videoCapture"],
|
|
972
1014
|
optionalPermissions: ["speakerSelection"],
|
|
1015
|
+
targetId: params.targetId,
|
|
973
1016
|
timeoutMs: Math.min(params.timeoutMs, 5e3)
|
|
974
1017
|
},
|
|
975
1018
|
timeoutMs: Math.min(params.timeoutMs, 5e3)
|
|
@@ -983,47 +1026,67 @@ function meetStatusScript(params) {
|
|
|
983
1026
|
const text = (node) => (node?.innerText || node?.textContent || "").trim();
|
|
984
1027
|
const allowMicrophone = ${JSON.stringify(params.allowMicrophone)};
|
|
985
1028
|
const captureCaptions = ${JSON.stringify(params.captureCaptions)};
|
|
1029
|
+
const readOnly = ${JSON.stringify(Boolean(params.readOnly))};
|
|
986
1030
|
const buttons = [...document.querySelectorAll('button')];
|
|
1031
|
+
const buttonLabel = (button) =>
|
|
1032
|
+
[
|
|
1033
|
+
button.getAttribute("aria-label"),
|
|
1034
|
+
button.getAttribute("data-tooltip"),
|
|
1035
|
+
text(button),
|
|
1036
|
+
]
|
|
1037
|
+
.filter(Boolean)
|
|
1038
|
+
.join(" ");
|
|
1039
|
+
const buttonLabels = buttons.map(buttonLabel).filter(Boolean);
|
|
987
1040
|
const notes = [];
|
|
988
1041
|
const findButton = (pattern) =>
|
|
989
1042
|
buttons.find((button) => {
|
|
990
|
-
const label =
|
|
991
|
-
button.getAttribute("aria-label"),
|
|
992
|
-
button.getAttribute("data-tooltip"),
|
|
993
|
-
text(button),
|
|
994
|
-
]
|
|
995
|
-
.filter(Boolean)
|
|
996
|
-
.join(" ");
|
|
1043
|
+
const label = buttonLabel(button);
|
|
997
1044
|
return pattern.test(label) && !button.disabled;
|
|
998
1045
|
});
|
|
1046
|
+
const findCallControlButton = (pattern) =>
|
|
1047
|
+
buttons.find((button) => {
|
|
1048
|
+
const label = buttonLabel(button);
|
|
1049
|
+
return pattern.test(label) && !/remotely mute|someone else/i.test(label) && !button.disabled;
|
|
1050
|
+
});
|
|
999
1051
|
const input = [...document.querySelectorAll('input')].find((el) =>
|
|
1000
1052
|
/your name/i.test(el.getAttribute('aria-label') || el.placeholder || '')
|
|
1001
1053
|
);
|
|
1002
|
-
if (${JSON.stringify(params.autoJoin)} && input && !input.value) {
|
|
1054
|
+
if (!readOnly && ${JSON.stringify(params.autoJoin)} && input && !input.value) {
|
|
1003
1055
|
input.focus();
|
|
1004
1056
|
input.value = ${JSON.stringify(params.guestName)};
|
|
1005
1057
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1006
1058
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1007
1059
|
}
|
|
1008
1060
|
const pageText = text(document.body).toLowerCase();
|
|
1061
|
+
const permissionText = [pageText, ...buttonLabels].join("\\n");
|
|
1009
1062
|
const host = location.hostname.toLowerCase();
|
|
1010
1063
|
const pageUrl = location.href;
|
|
1011
|
-
const permissionNeeded = /permission needed|allow.*(microphone|camera)|blocked.*(microphone|camera)|permission.*(microphone|camera|speaker)/i.test(
|
|
1012
|
-
|
|
1013
|
-
if (!
|
|
1064
|
+
const permissionNeeded = /permission needed|microphone problem|speaker problem|allow.*(microphone|camera)|blocked.*(microphone|camera)|permission.*(microphone|camera|speaker)/i.test(permissionText);
|
|
1065
|
+
let mic = findCallControlButton(/^\\s*turn (?:off|on) microphone\\b/i);
|
|
1066
|
+
if (!mic) {
|
|
1067
|
+
const callControls = document.querySelector('[role="region"][aria-label="Call controls"]');
|
|
1068
|
+
mic = [...(callControls?.querySelectorAll('button') || [])].find((button) =>
|
|
1069
|
+
/^\\s*turn (?:off|on) microphone\\b/i.test(buttonLabel(button))
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
if (!readOnly && allowMicrophone && mic && /turn on microphone/i.test(buttonLabel(mic))) {
|
|
1073
|
+
mic.click();
|
|
1074
|
+
notes.push("Attempted to turn on the Meet microphone for realtime mode.");
|
|
1075
|
+
}
|
|
1076
|
+
if (!readOnly && !allowMicrophone && mic && /turn off microphone/i.test(mic.getAttribute('aria-label') || text(mic))) {
|
|
1014
1077
|
mic.click();
|
|
1015
1078
|
notes.push("Muted Meet microphone for observe-only mode.");
|
|
1016
1079
|
}
|
|
1017
|
-
const join = ${JSON.stringify(params.autoJoin)}
|
|
1080
|
+
const join = !readOnly && ${JSON.stringify(params.autoJoin)}
|
|
1018
1081
|
? findButton(/join now|ask to join/i)
|
|
1019
1082
|
: null;
|
|
1020
1083
|
if (join) join.click();
|
|
1021
1084
|
const microphoneChoice = findButton(/\\buse microphone\\b/i);
|
|
1022
1085
|
const noMicrophoneChoice = findButton(/\\b(continue|join|use) without (microphone|mic)\\b|\\bnot now\\b/i);
|
|
1023
|
-
if (allowMicrophone && microphoneChoice) {
|
|
1086
|
+
if (!readOnly && allowMicrophone && microphoneChoice) {
|
|
1024
1087
|
microphoneChoice.click();
|
|
1025
1088
|
notes.push("Accepted Meet microphone prompt with browser automation.");
|
|
1026
|
-
} else if (!allowMicrophone && noMicrophoneChoice) {
|
|
1089
|
+
} else if (!readOnly && !allowMicrophone && noMicrophoneChoice) {
|
|
1027
1090
|
noMicrophoneChoice.click();
|
|
1028
1091
|
notes.push("Skipped Meet microphone prompt for observe-only mode.");
|
|
1029
1092
|
}
|
|
@@ -1078,7 +1141,7 @@ function meetStatusScript(params) {
|
|
|
1078
1141
|
}
|
|
1079
1142
|
};
|
|
1080
1143
|
if (captionState) {
|
|
1081
|
-
if (inCall && !captionState.enabledAttempted) {
|
|
1144
|
+
if (!readOnly && inCall && !captionState.enabledAttempted) {
|
|
1082
1145
|
const captionButton = findButton(/turn on captions|show captions|captions/i);
|
|
1083
1146
|
const captionLabel = captionButton ? (captionButton.getAttribute("aria-label") || captionButton.getAttribute("data-tooltip") || text(captionButton)) : "";
|
|
1084
1147
|
if (captionButton) {
|
|
@@ -1140,7 +1203,7 @@ function meetStatusScript(params) {
|
|
|
1140
1203
|
clickedJoin: Boolean(join),
|
|
1141
1204
|
clickedMicrophoneChoice: Boolean(allowMicrophone && microphoneChoice),
|
|
1142
1205
|
inCall,
|
|
1143
|
-
micMuted: mic ? /turn on microphone/i.test(
|
|
1206
|
+
micMuted: mic ? /turn on microphone/i.test(buttonLabel(mic)) : undefined,
|
|
1144
1207
|
lobbyWaiting,
|
|
1145
1208
|
leaveReason,
|
|
1146
1209
|
captioning,
|
|
@@ -1214,6 +1277,7 @@ async function openMeetWithBrowserRequest(params) {
|
|
|
1214
1277
|
const permissionNotes = await grantMeetMediaPermissions({
|
|
1215
1278
|
allowMicrophone: params.mode === "realtime",
|
|
1216
1279
|
callBrowser: params.callBrowser,
|
|
1280
|
+
targetId,
|
|
1217
1281
|
timeoutMs
|
|
1218
1282
|
});
|
|
1219
1283
|
const deadline = Date.now() + Math.max(0, params.config.chrome.waitForInCallMs);
|
|
@@ -1240,7 +1304,7 @@ async function openMeetWithBrowserRequest(params) {
|
|
|
1240
1304
|
},
|
|
1241
1305
|
timeoutMs: Math.min(timeoutMs, 1e4)
|
|
1242
1306
|
})) ?? browser, permissionNotes);
|
|
1243
|
-
if (browser?.inCall === true) return {
|
|
1307
|
+
if (browser?.inCall === true && (params.mode !== "realtime" || browser.micMuted !== true)) return {
|
|
1244
1308
|
launched: true,
|
|
1245
1309
|
browser
|
|
1246
1310
|
};
|
|
@@ -1279,9 +1343,10 @@ async function inspectRecoverableMeetTab(params) {
|
|
|
1279
1343
|
body: { targetId: params.targetId },
|
|
1280
1344
|
timeoutMs: Math.min(params.timeoutMs, 5e3)
|
|
1281
1345
|
});
|
|
1282
|
-
const permissionNotes = await grantMeetMediaPermissions({
|
|
1346
|
+
const permissionNotes = params.readOnly ? [] : await grantMeetMediaPermissions({
|
|
1283
1347
|
allowMicrophone,
|
|
1284
1348
|
callBrowser: params.callBrowser,
|
|
1349
|
+
targetId: params.targetId,
|
|
1285
1350
|
timeoutMs: params.timeoutMs
|
|
1286
1351
|
});
|
|
1287
1352
|
const browser = mergeBrowserNotes(parseMeetBrowserStatus(await params.callBrowser({
|
|
@@ -1294,7 +1359,8 @@ async function inspectRecoverableMeetTab(params) {
|
|
|
1294
1359
|
allowMicrophone,
|
|
1295
1360
|
captureCaptions: params.mode === "transcribe",
|
|
1296
1361
|
guestName: params.config.chrome.guestName,
|
|
1297
|
-
autoJoin: false
|
|
1362
|
+
autoJoin: false,
|
|
1363
|
+
readOnly: params.readOnly
|
|
1298
1364
|
})
|
|
1299
1365
|
},
|
|
1300
1366
|
timeoutMs: Math.min(params.timeoutMs, 1e4)
|
|
@@ -1332,6 +1398,7 @@ async function recoverCurrentMeetTab(params) {
|
|
|
1332
1398
|
callBrowser: callLocalBrowserRequest,
|
|
1333
1399
|
config: params.config,
|
|
1334
1400
|
mode: params.mode,
|
|
1401
|
+
readOnly: params.readOnly,
|
|
1335
1402
|
timeoutMs,
|
|
1336
1403
|
tab,
|
|
1337
1404
|
targetId
|
|
@@ -1373,6 +1440,7 @@ async function recoverCurrentMeetTabOnNode(params) {
|
|
|
1373
1440
|
}),
|
|
1374
1441
|
config: params.config,
|
|
1375
1442
|
mode: params.mode,
|
|
1443
|
+
readOnly: params.readOnly,
|
|
1376
1444
|
timeoutMs,
|
|
1377
1445
|
tab,
|
|
1378
1446
|
targetId
|
|
@@ -2163,6 +2231,11 @@ function evaluateSpeechReadiness(session) {
|
|
|
2163
2231
|
message: health.manualActionMessage ?? "Resolve the Google Meet browser prompt before asking OpenClaw to speak."
|
|
2164
2232
|
};
|
|
2165
2233
|
if (health?.inCall === true) {
|
|
2234
|
+
if (health.micMuted === true) return {
|
|
2235
|
+
ready: false,
|
|
2236
|
+
reason: "meet-microphone-muted",
|
|
2237
|
+
message: "Turn on the OpenClaw Google Meet microphone before asking OpenClaw to speak."
|
|
2238
|
+
};
|
|
2166
2239
|
if (session.chrome.audioBridge) return { ready: true };
|
|
2167
2240
|
return {
|
|
2168
2241
|
ready: false,
|
|
@@ -2214,14 +2287,14 @@ var GoogleMeetRuntime = class {
|
|
|
2214
2287
|
this.#refreshHealth(sessionId);
|
|
2215
2288
|
if (!sessionId) {
|
|
2216
2289
|
const sessions = [...this.#sessions.values()].toSorted((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
2217
|
-
await Promise.all(sessions.map((session) => this.#
|
|
2290
|
+
await Promise.all(sessions.map((session) => this.#refreshStatusHealthForSession(session)));
|
|
2218
2291
|
return {
|
|
2219
2292
|
found: true,
|
|
2220
2293
|
sessions
|
|
2221
2294
|
};
|
|
2222
2295
|
}
|
|
2223
2296
|
const session = this.#sessions.get(sessionId);
|
|
2224
|
-
if (session) await this.#
|
|
2297
|
+
if (session) await this.#refreshStatusHealthForSession(session);
|
|
2225
2298
|
return session ? {
|
|
2226
2299
|
found: true,
|
|
2227
2300
|
session
|
|
@@ -2321,7 +2394,7 @@ var GoogleMeetRuntime = class {
|
|
|
2321
2394
|
reusable.updatedAt = nowIso();
|
|
2322
2395
|
return {
|
|
2323
2396
|
session: reusable,
|
|
2324
|
-
spoken: mode === "realtime" && speechInstructions ?
|
|
2397
|
+
spoken: mode === "realtime" && speechInstructions ? await this.#speakWhenReady(reusable, speechInstructions) : false
|
|
2325
2398
|
};
|
|
2326
2399
|
}
|
|
2327
2400
|
const createdAt = nowIso();
|
|
@@ -2367,17 +2440,9 @@ var GoogleMeetRuntime = class {
|
|
|
2367
2440
|
launched: result.launched,
|
|
2368
2441
|
nodeId: "nodeId" in result ? result.nodeId : void 0,
|
|
2369
2442
|
browserProfile: this.params.config.chrome.browserProfile,
|
|
2370
|
-
audioBridge: result.audioBridge ? {
|
|
2371
|
-
type: result.audioBridge.type,
|
|
2372
|
-
provider: result.audioBridge.type === "command-pair" || result.audioBridge.type === "node-command-pair" ? result.audioBridge.providerId : void 0
|
|
2373
|
-
} : void 0,
|
|
2374
2443
|
health: "browser" in result ? result.browser : void 0
|
|
2375
2444
|
};
|
|
2376
|
-
|
|
2377
|
-
this.#sessionStops.set(session.id, result.audioBridge.stop);
|
|
2378
|
-
this.#sessionSpeakers.set(session.id, result.audioBridge.speak);
|
|
2379
|
-
this.#sessionHealth.set(session.id, result.audioBridge.getHealth);
|
|
2380
|
-
}
|
|
2445
|
+
this.#attachChromeAudioBridge(session, result.audioBridge);
|
|
2381
2446
|
session.notes.push(result.audioBridge ? 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." : mode === "realtime" ? "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.");
|
|
2382
2447
|
this.#refreshSpeechReadiness(session);
|
|
2383
2448
|
} else {
|
|
@@ -2418,7 +2483,7 @@ var GoogleMeetRuntime = class {
|
|
|
2418
2483
|
this.#sessions.set(session.id, session);
|
|
2419
2484
|
return {
|
|
2420
2485
|
session,
|
|
2421
|
-
spoken: transport === "twilio" ? delegatedTwilioSpoken : mode === "realtime" && speechInstructions ?
|
|
2486
|
+
spoken: transport === "twilio" ? delegatedTwilioSpoken : mode === "realtime" && speechInstructions ? await this.#speakWhenReady(session, speechInstructions) : false
|
|
2422
2487
|
};
|
|
2423
2488
|
}
|
|
2424
2489
|
async leave(sessionId) {
|
|
@@ -2459,6 +2524,7 @@ var GoogleMeetRuntime = class {
|
|
|
2459
2524
|
};
|
|
2460
2525
|
}
|
|
2461
2526
|
await this.#refreshBrowserHealthForChromeSession(session);
|
|
2527
|
+
await this.#ensureChromeRealtimeBridge(session);
|
|
2462
2528
|
const speak = this.#sessionSpeakers.get(sessionId);
|
|
2463
2529
|
if (!speak || session.state !== "active") return {
|
|
2464
2530
|
found: true,
|
|
@@ -2485,6 +2551,22 @@ var GoogleMeetRuntime = class {
|
|
|
2485
2551
|
session
|
|
2486
2552
|
};
|
|
2487
2553
|
}
|
|
2554
|
+
async #speakWhenReady(session, instructions) {
|
|
2555
|
+
let result = await this.speak(session.id, instructions);
|
|
2556
|
+
if (result.spoken || session.transport === "twilio") return result.spoken;
|
|
2557
|
+
const waitMs = Math.min(Math.max(0, this.params.config.chrome.waitForInCallMs), Math.max(0, this.params.config.chrome.joinTimeoutMs));
|
|
2558
|
+
const deadline = Date.now() + waitMs;
|
|
2559
|
+
while (Date.now() < deadline) {
|
|
2560
|
+
await sleep(250);
|
|
2561
|
+
result = await this.speak(session.id, instructions);
|
|
2562
|
+
if (result.spoken) return true;
|
|
2563
|
+
const health = result.session?.chrome?.health;
|
|
2564
|
+
if (health?.manualActionRequired || result.session?.state !== "active") return false;
|
|
2565
|
+
const blocked = health?.speechBlockedReason;
|
|
2566
|
+
if (blocked && blocked !== "not-in-call" && blocked !== "browser-unverified") return false;
|
|
2567
|
+
}
|
|
2568
|
+
return false;
|
|
2569
|
+
}
|
|
2488
2570
|
async testSpeech(request) {
|
|
2489
2571
|
if (request.mode === "transcribe") throw new Error("test_speech requires mode: realtime; use join mode: transcribe for observe-only sessions.");
|
|
2490
2572
|
const url = normalizeMeetUrl(request.url);
|
|
@@ -2581,12 +2663,26 @@ var GoogleMeetRuntime = class {
|
|
|
2581
2663
|
}
|
|
2582
2664
|
await this.#refreshBrowserHealthForChromeSession(session);
|
|
2583
2665
|
}
|
|
2584
|
-
async #
|
|
2666
|
+
async #refreshStatusHealthForSession(session) {
|
|
2667
|
+
if (session.transport === "chrome" || session.transport === "chrome-node") {
|
|
2668
|
+
if (session.chrome?.health?.manualActionRequired) {
|
|
2669
|
+
this.#refreshSpeechReadiness(session);
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
await this.#refreshBrowserHealthForChromeSession(session, {
|
|
2673
|
+
force: true,
|
|
2674
|
+
readOnly: true
|
|
2675
|
+
});
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
this.#refreshSpeechReadiness(session);
|
|
2679
|
+
}
|
|
2680
|
+
async #refreshBrowserHealthForChromeSession(session, options = {}) {
|
|
2585
2681
|
if (!isManagedChromeBrowserSession(session)) {
|
|
2586
2682
|
this.#refreshSpeechReadiness(session);
|
|
2587
2683
|
return;
|
|
2588
2684
|
}
|
|
2589
|
-
if (session.mode === "realtime" && evaluateSpeechReadiness(session).ready) {
|
|
2685
|
+
if (!options.force && session.mode === "realtime" && evaluateSpeechReadiness(session).ready) {
|
|
2590
2686
|
this.#refreshSpeechReadiness(session);
|
|
2591
2687
|
return;
|
|
2592
2688
|
}
|
|
@@ -2595,10 +2691,12 @@ var GoogleMeetRuntime = class {
|
|
|
2595
2691
|
runtime: this.params.runtime,
|
|
2596
2692
|
config: this.params.config,
|
|
2597
2693
|
mode: session.mode,
|
|
2694
|
+
readOnly: options.readOnly,
|
|
2598
2695
|
url: session.url
|
|
2599
2696
|
}) : await recoverCurrentMeetTab({
|
|
2600
2697
|
config: this.params.config,
|
|
2601
2698
|
mode: session.mode,
|
|
2699
|
+
readOnly: options.readOnly,
|
|
2602
2700
|
url: session.url
|
|
2603
2701
|
});
|
|
2604
2702
|
if (result.found && result.browser && session.chrome) {
|
|
@@ -2613,8 +2711,43 @@ var GoogleMeetRuntime = class {
|
|
|
2613
2711
|
}
|
|
2614
2712
|
this.#refreshSpeechReadiness(session);
|
|
2615
2713
|
}
|
|
2714
|
+
#attachChromeAudioBridge(session, audioBridge) {
|
|
2715
|
+
if (!session.chrome || !audioBridge) return;
|
|
2716
|
+
session.chrome.audioBridge = {
|
|
2717
|
+
type: audioBridge.type,
|
|
2718
|
+
provider: audioBridge.type === "command-pair" || audioBridge.type === "node-command-pair" ? audioBridge.providerId : void 0
|
|
2719
|
+
};
|
|
2720
|
+
if (audioBridge.type === "command-pair" || audioBridge.type === "node-command-pair") {
|
|
2721
|
+
this.#sessionStops.set(session.id, audioBridge.stop);
|
|
2722
|
+
this.#sessionSpeakers.set(session.id, audioBridge.speak);
|
|
2723
|
+
this.#sessionHealth.set(session.id, audioBridge.getHealth);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
async #ensureChromeRealtimeBridge(session) {
|
|
2727
|
+
if (session.mode !== "realtime" || session.transport !== "chrome" || session.state !== "active" || !session.chrome || session.chrome.audioBridge) return;
|
|
2728
|
+
const health = session.chrome.health;
|
|
2729
|
+
if (health?.inCall !== true || health.micMuted === true || health.manualActionRequired === true) return;
|
|
2730
|
+
const result = await launchChromeMeet({
|
|
2731
|
+
runtime: this.params.runtime,
|
|
2732
|
+
config: {
|
|
2733
|
+
...this.params.config,
|
|
2734
|
+
chrome: {
|
|
2735
|
+
...this.params.config.chrome,
|
|
2736
|
+
launch: false
|
|
2737
|
+
}
|
|
2738
|
+
},
|
|
2739
|
+
fullConfig: this.params.fullConfig,
|
|
2740
|
+
meetingSessionId: session.id,
|
|
2741
|
+
mode: session.mode,
|
|
2742
|
+
url: session.url,
|
|
2743
|
+
logger: this.params.logger
|
|
2744
|
+
});
|
|
2745
|
+
this.#attachChromeAudioBridge(session, result.audioBridge);
|
|
2746
|
+
session.updatedAt = nowIso();
|
|
2747
|
+
}
|
|
2616
2748
|
#refreshSpeechReadiness(session) {
|
|
2617
2749
|
const readiness = evaluateSpeechReadiness(session);
|
|
2750
|
+
if (readiness.ready) session.notes = session.notes.filter((note) => !note.startsWith("Realtime speech blocked:"));
|
|
2618
2751
|
if (session.chrome) session.chrome.health = {
|
|
2619
2752
|
...session.chrome.health,
|
|
2620
2753
|
speechReady: readiness.ready,
|
|
@@ -3080,7 +3213,7 @@ async function exportGoogleMeetBundleFromParams(config, raw) {
|
|
|
3080
3213
|
lateAfterMinutes: resolved.lateAfterMinutes,
|
|
3081
3214
|
earlyBeforeMinutes: resolved.earlyBeforeMinutes
|
|
3082
3215
|
})]);
|
|
3083
|
-
const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } = await import("./cli-
|
|
3216
|
+
const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } = await import("./cli-B_wJa8XB.js");
|
|
3084
3217
|
const calendarId = normalizeOptionalString(raw.calendarId);
|
|
3085
3218
|
const request = {
|
|
3086
3219
|
...resolved.meeting ? { meeting: resolved.meeting } : {},
|
|
@@ -3489,7 +3622,7 @@ var google_meet_default = definePluginEntry({
|
|
|
3489
3622
|
handle: handleGoogleMeetNodeHostCommand
|
|
3490
3623
|
});
|
|
3491
3624
|
api.registerCli(async ({ program }) => {
|
|
3492
|
-
const { registerGoogleMeetCli } = await import("./cli-
|
|
3625
|
+
const { registerGoogleMeetCli } = await import("./cli-B_wJa8XB.js");
|
|
3493
3626
|
registerGoogleMeetCli({
|
|
3494
3627
|
program,
|
|
3495
3628
|
config,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/google-meet",
|
|
3
|
-
"version": "2026.5.3
|
|
3
|
+
"version": "2026.5.3",
|
|
4
4
|
"description": "OpenClaw Google Meet participant plugin",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"openclaw": "workspace:*"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
|
-
"openclaw": ">=2026.5.3
|
|
19
|
+
"openclaw": ">=2026.5.3"
|
|
20
20
|
},
|
|
21
21
|
"peerDependenciesMeta": {
|
|
22
22
|
"openclaw": {
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"minHostVersion": ">=2026.4.20"
|
|
34
34
|
},
|
|
35
35
|
"compat": {
|
|
36
|
-
"pluginApi": ">=2026.5.3
|
|
36
|
+
"pluginApi": ">=2026.5.3"
|
|
37
37
|
},
|
|
38
38
|
"build": {
|
|
39
|
-
"openclawVersion": "2026.5.3
|
|
39
|
+
"openclawVersion": "2026.5.3"
|
|
40
40
|
},
|
|
41
41
|
"release": {
|
|
42
42
|
"publishToClawHub": true,
|