mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
7d4066639e
* feat(meetings): add Linux audio routing * chore(plugin-sdk): refresh meeting audio API baseline
294 lines
10 KiB
TypeScript
294 lines
10 KiB
TypeScript
import { parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
import type { GoogleMeetCliCommandContext } from "./cli-command-context.js";
|
|
import {
|
|
callGoogleMeetGateway,
|
|
parseGoogleMeetBrowserTransport,
|
|
parseGoogleMeetMode,
|
|
parseGoogleMeetTransport,
|
|
parsePositiveNumber,
|
|
type JoinOptions,
|
|
type RecoverTabOptions,
|
|
type SetupOptions,
|
|
writeLeaveResult,
|
|
writeRecoverCurrentTabResult,
|
|
writeSetupStatus,
|
|
writeStdoutJson,
|
|
writeStdoutLine,
|
|
} from "./cli-shared.js";
|
|
import type { GoogleMeetRuntime } from "./runtime.js";
|
|
|
|
export function registerGoogleMeetProbeCommands(context: GoogleMeetCliCommandContext): void {
|
|
const params = context;
|
|
const { root, callGateway, operationTimeoutMs, resolveMeetingInput } = context;
|
|
|
|
root
|
|
.command("join")
|
|
.argument("[url]", "Explicit https://meet.google.com/... URL")
|
|
.option("--transport <transport>", "Transport: chrome, chrome-node, or twilio")
|
|
.option("--mode <mode>", "Mode: agent, bidi, or transcribe")
|
|
.option("--message <text>", "Realtime speech to trigger after join")
|
|
.option("--dial-in-number <phone>", "Meet dial-in number for Twilio transport")
|
|
.option("--pin <pin>", "Meet phone PIN; # is appended if omitted")
|
|
.option("--dtmf-sequence <sequence>", "Explicit Twilio DTMF sequence")
|
|
.action(async (url: string | undefined, options: JoinOptions) => {
|
|
const payload = {
|
|
url: resolveMeetingInput(params.config, url),
|
|
transport: parseGoogleMeetTransport(options.transport),
|
|
mode: parseGoogleMeetMode(options.mode),
|
|
message: options.message,
|
|
dialInNumber: options.dialInNumber,
|
|
pin: options.pin,
|
|
dtmfSequence: options.dtmfSequence,
|
|
};
|
|
const delegated = await callGoogleMeetGateway({
|
|
callGateway,
|
|
method: "googlemeet.join",
|
|
payload,
|
|
timeoutMs: operationTimeoutMs,
|
|
});
|
|
if (delegated.ok) {
|
|
const result = delegated.payload as { session?: unknown };
|
|
writeStdoutJson(result.session ?? delegated.payload);
|
|
return;
|
|
}
|
|
const rt = await params.ensureRuntime();
|
|
const result = await rt.join(payload);
|
|
writeStdoutJson(result.session);
|
|
});
|
|
|
|
root
|
|
.command("test-speech")
|
|
.argument("[url]", "Explicit https://meet.google.com/... URL")
|
|
.option("--transport <transport>", "Transport: chrome, chrome-node, or twilio")
|
|
.option("--mode <mode>", "Mode: agent, bidi, or transcribe")
|
|
.option(
|
|
"--message <text>",
|
|
"Realtime speech to trigger",
|
|
"Say exactly: Google Meet speech test complete.",
|
|
)
|
|
.action(async (url: string | undefined, options: JoinOptions) => {
|
|
const payload = {
|
|
url: resolveMeetingInput(params.config, url),
|
|
transport: parseGoogleMeetTransport(options.transport),
|
|
mode: parseGoogleMeetMode(options.mode),
|
|
message: options.message,
|
|
};
|
|
const delegated = await callGoogleMeetGateway({
|
|
callGateway,
|
|
method: "googlemeet.testSpeech",
|
|
payload,
|
|
timeoutMs: operationTimeoutMs,
|
|
});
|
|
if (delegated.ok) {
|
|
writeStdoutJson(delegated.payload);
|
|
return;
|
|
}
|
|
const rt = await params.ensureRuntime();
|
|
writeStdoutJson(await rt.testSpeech(payload));
|
|
});
|
|
|
|
root
|
|
.command("test-listen")
|
|
.argument("[url]", "Explicit https://meet.google.com/... URL")
|
|
.option("--transport <transport>", "Transport: chrome or chrome-node")
|
|
.option("--timeout-ms <ms>", "How long to wait for fresh captions/transcript movement")
|
|
.action(async (url: string | undefined, options: JoinOptions) => {
|
|
const payload = {
|
|
url: resolveMeetingInput(params.config, url),
|
|
transport: parseGoogleMeetBrowserTransport(options.transport),
|
|
timeoutMs: parsePositiveNumber(options.timeoutMs, "timeout-ms"),
|
|
};
|
|
const delegated = await callGoogleMeetGateway({
|
|
callGateway,
|
|
method: "googlemeet.testListen",
|
|
payload,
|
|
timeoutMs: operationTimeoutMs,
|
|
});
|
|
if (delegated.ok) {
|
|
writeStdoutJson(delegated.payload);
|
|
return;
|
|
}
|
|
const rt = await params.ensureRuntime();
|
|
writeStdoutJson(await rt.testListen(payload));
|
|
});
|
|
}
|
|
|
|
export function registerGoogleMeetSessionCommands(context: GoogleMeetCliCommandContext): void {
|
|
const params = context;
|
|
const { root, callGateway } = context;
|
|
|
|
root
|
|
.command("status")
|
|
.argument("[session-id]", "Meet session ID")
|
|
.option("--json", "Print JSON output", false)
|
|
.action(async (sessionId?: string) => {
|
|
const delegated = await callGoogleMeetGateway({
|
|
callGateway,
|
|
method: "googlemeet.status",
|
|
payload: { sessionId },
|
|
});
|
|
if (delegated.ok) {
|
|
writeStdoutJson(delegated.payload);
|
|
return;
|
|
}
|
|
const rt = await params.ensureRuntime();
|
|
writeStdoutJson(await rt.status(sessionId));
|
|
});
|
|
|
|
root
|
|
.command("transcript")
|
|
.description("Print the bounded caption transcript for a Meet session")
|
|
.argument("<session-id>", "Meet session ID")
|
|
.option("--since <index>", "Resume from the previous response's nextIndex")
|
|
.option("--json", "Print JSON output", false)
|
|
.action(async (sessionId: string, options: { since?: string; json?: boolean }) => {
|
|
const sinceIndex = parseStrictNonNegativeInteger(options.since);
|
|
if (options.since !== undefined && sinceIndex === undefined) {
|
|
throw new Error("--since must be a non-negative safe integer");
|
|
}
|
|
const delegated = await callGoogleMeetGateway({
|
|
callGateway,
|
|
method: "googlemeet.transcript",
|
|
payload: { sessionId, ...(sinceIndex === undefined ? {} : { sinceIndex }) },
|
|
});
|
|
const result = delegated.ok
|
|
? (delegated.payload as Awaited<ReturnType<GoogleMeetRuntime["transcript"]>>)
|
|
: await (
|
|
await params.ensureRuntime()
|
|
).transcript(sessionId, sinceIndex === undefined ? {} : { sinceIndex });
|
|
if (!result.found) {
|
|
throw new Error("session not found");
|
|
}
|
|
if (options.json) {
|
|
writeStdoutJson(result);
|
|
return;
|
|
}
|
|
if (result.evicted) {
|
|
writeStdoutLine("# transcript evicted from runtime memory");
|
|
} else if (result.droppedLines) {
|
|
writeStdoutLine("# %d earlier lines dropped by the transcript cap", result.droppedLines);
|
|
}
|
|
for (const line of result.lines ?? []) {
|
|
writeStdoutLine(
|
|
"%s%s%s",
|
|
line.at ? `[${line.at}] ` : "",
|
|
line.speaker ? `${line.speaker}: ` : "",
|
|
line.text,
|
|
);
|
|
}
|
|
writeStdoutLine("# nextIndex: %d", result.nextIndex ?? 0);
|
|
});
|
|
}
|
|
|
|
export function registerGoogleMeetLifecycleCommands(context: GoogleMeetCliCommandContext): void {
|
|
const params = context;
|
|
const { root, callGateway } = context;
|
|
|
|
root
|
|
.command("recover-tab")
|
|
.description("Focus and inspect an existing Google Meet tab")
|
|
.argument("[url]", "Optional Meet URL to match")
|
|
.option("--transport <transport>", "Transport to inspect: chrome or chrome-node")
|
|
.option("--json", "Print JSON output", false)
|
|
.action(async (url: string | undefined, options: RecoverTabOptions) => {
|
|
const rt = await params.ensureRuntime();
|
|
const result = await rt.recoverCurrentTab({
|
|
url,
|
|
transport: parseGoogleMeetBrowserTransport(options.transport),
|
|
});
|
|
if (options.json) {
|
|
writeStdoutJson(result);
|
|
return;
|
|
}
|
|
writeRecoverCurrentTabResult(result);
|
|
});
|
|
|
|
root
|
|
.command("setup")
|
|
.description("Show Google Meet transport setup status")
|
|
.option("--transport <transport>", "Transport to check: chrome, chrome-node, or twilio")
|
|
.option("--mode <mode>", "Mode to check: agent, bidi, or transcribe")
|
|
.option("--json", "Print JSON output", false)
|
|
.action(async (options: SetupOptions) => {
|
|
const rt = await params.ensureRuntime();
|
|
const status = await rt.setupStatus({
|
|
transport: parseGoogleMeetTransport(options.transport),
|
|
mode: parseGoogleMeetMode(options.mode),
|
|
});
|
|
if (options.json) {
|
|
writeStdoutJson(status);
|
|
if (!status.ok) {
|
|
process.exitCode = 1;
|
|
}
|
|
return;
|
|
}
|
|
writeSetupStatus(status);
|
|
if (!status.ok) {
|
|
process.exitCode = 1;
|
|
}
|
|
});
|
|
|
|
root
|
|
.command("leave")
|
|
.argument("<session-id>", "Meet session ID")
|
|
.action(async (sessionId: string) => {
|
|
const delegated = await callGoogleMeetGateway({
|
|
callGateway,
|
|
method: "googlemeet.leave",
|
|
payload: { sessionId },
|
|
});
|
|
if (delegated.ok) {
|
|
const result = delegated.payload as { found?: boolean; browserLeft?: boolean };
|
|
if (!result.found) {
|
|
throw new Error("session not found");
|
|
}
|
|
writeLeaveResult(sessionId, result);
|
|
return;
|
|
}
|
|
const rt = await params.ensureRuntime();
|
|
const result = await rt.leave(sessionId);
|
|
if (!result.found) {
|
|
throw new Error("session not found");
|
|
}
|
|
writeLeaveResult(sessionId, result);
|
|
});
|
|
|
|
root
|
|
.command("speak")
|
|
.argument("<session-id>", "Meet session ID")
|
|
.argument("[message]", "Realtime instructions to speak now")
|
|
.action(async (sessionId: string, message?: string) => {
|
|
const delegated = await callGoogleMeetGateway({
|
|
callGateway,
|
|
method: "googlemeet.speak",
|
|
payload: { sessionId, message },
|
|
});
|
|
if (delegated.ok) {
|
|
const result = delegated.payload as Awaited<ReturnType<GoogleMeetRuntime["speak"]>>;
|
|
if (!result.found) {
|
|
throw new Error("session not found");
|
|
}
|
|
if (!result.spoken) {
|
|
throw new Error(
|
|
result.session?.chrome?.health?.speechBlockedMessage ??
|
|
"session has no active realtime audio bridge",
|
|
);
|
|
}
|
|
writeStdoutLine("speaking on %s", sessionId);
|
|
return;
|
|
}
|
|
const rt = await params.ensureRuntime();
|
|
const result = await rt.speak(sessionId, message);
|
|
if (!result.found) {
|
|
throw new Error("session not found");
|
|
}
|
|
if (!result.spoken) {
|
|
throw new Error(
|
|
result.session?.chrome?.health?.speechBlockedMessage ??
|
|
"session has no active realtime audio bridge",
|
|
);
|
|
}
|
|
writeStdoutLine("speaking on %s", sessionId);
|
|
});
|
|
}
|