mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:50:43 +00:00
feat(google-meet): add latest conference command
This commit is contained in:
@@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/Google Meet: add a `chrome-node` transport so a paired macOS node, such as a Parallels VM, can own Chrome, BlackHole, and SoX while the Gateway machine keeps the agent and model key. Thanks @steipete.
|
||||
- Plugins/Google Meet: add `googlemeet artifacts` and `googlemeet attendance` commands plus matching tool/gateway actions for conference records, recordings, transcripts and transcript entries, smart notes, and participant sessions. Thanks @steipete.
|
||||
- Plugins/Google Meet: add markdown and file output for `googlemeet artifacts` and `googlemeet attendance` reports. Thanks @steipete.
|
||||
- Plugins/Google Meet: add `googlemeet latest` plus matching tool/gateway actions to find the newest conference record for a meeting. Thanks @steipete.
|
||||
- Plugins/Google Meet: add `googlemeet doctor --oauth` so operators can verify OAuth token refresh, Meet space reads, and side-effecting space creation without printing secrets. Thanks @steipete.
|
||||
- Plugins/Voice Call: expose the shared `openclaw_agent_consult` realtime tool so live phone calls can ask the full OpenClaw agent for deeper/tool-backed answers. Thanks @steipete.
|
||||
- Plugins/Voice Call: add `voicecall setup` and a dry-run-by-default `voicecall smoke` command so Twilio/provider readiness can be checked before placing a live test call. Thanks @steipete.
|
||||
|
||||
@@ -638,6 +638,7 @@ openclaw googlemeet attendance --meeting https://meet.google.com/abc-defg-hij
|
||||
If you already know the conference record id, address it directly:
|
||||
|
||||
```bash
|
||||
openclaw googlemeet latest --meeting https://meet.google.com/abc-defg-hij
|
||||
openclaw googlemeet artifacts --conference-record conferenceRecords/abc123 --json
|
||||
openclaw googlemeet attendance --conference-record conferenceRecords/abc123 --json
|
||||
```
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
createGoogleMeetSpace,
|
||||
fetchGoogleMeetArtifacts,
|
||||
fetchGoogleMeetAttendance,
|
||||
fetchLatestGoogleMeetConferenceRecord,
|
||||
fetchGoogleMeetSpace,
|
||||
normalizeGoogleMeetSpaceName,
|
||||
} from "./src/meet.js";
|
||||
@@ -343,6 +344,7 @@ describe("google-meet plugin", () => {
|
||||
"setup_status",
|
||||
"resolve_space",
|
||||
"preflight",
|
||||
"latest",
|
||||
"artifacts",
|
||||
"attendance",
|
||||
"recover_current_tab",
|
||||
@@ -496,6 +498,32 @@ describe("google-meet plugin", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("fetches only the latest Meet conference record for a meeting", async () => {
|
||||
const fetchMock = stubMeetArtifactsApi();
|
||||
|
||||
await expect(
|
||||
fetchLatestGoogleMeetConferenceRecord({
|
||||
accessToken: "token",
|
||||
meeting: "abc-defg-hij",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
input: "abc-defg-hij",
|
||||
space: { name: "spaces/abc-defg-hij" },
|
||||
conferenceRecord: { name: "conferenceRecords/rec-1" },
|
||||
});
|
||||
|
||||
const listCall = fetchMock.mock.calls.find(([input]) => {
|
||||
const url = requestUrl(input);
|
||||
return url.pathname === "/v2/conferenceRecords";
|
||||
});
|
||||
if (!listCall) {
|
||||
throw new Error("Expected conferenceRecords.list fetch call");
|
||||
}
|
||||
const listUrl = requestUrl(listCall[0]);
|
||||
expect(listUrl.searchParams.get("pageSize")).toBe("1");
|
||||
expect(listUrl.searchParams.get("filter")).toBe('space.name = "spaces/abc-defg-hij"');
|
||||
});
|
||||
|
||||
it("lists Meet attendance rows with participant sessions", async () => {
|
||||
const fetchMock = stubMeetArtifactsApi();
|
||||
|
||||
@@ -695,6 +723,26 @@ describe("google-meet plugin", () => {
|
||||
expect(result.details.attendance).toEqual([expect.objectContaining({ displayName: "Alice" })]);
|
||||
});
|
||||
|
||||
it("reports the latest conference record through the tool", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const { tools } = setup();
|
||||
const tool = tools[0] as {
|
||||
execute: (
|
||||
id: string,
|
||||
params: unknown,
|
||||
) => Promise<{ details: { conferenceRecord?: { name?: string } } }>;
|
||||
};
|
||||
|
||||
const result = await tool.execute("id", {
|
||||
action: "latest",
|
||||
accessToken: "token",
|
||||
expiresAt: Date.now() + 120_000,
|
||||
meeting: "abc-defg-hij",
|
||||
});
|
||||
|
||||
expect(result.details.conferenceRecord).toMatchObject({ name: "conferenceRecords/rec-1" });
|
||||
});
|
||||
|
||||
it("fails setup status when the configured Chrome node is not connected", async () => {
|
||||
const { tools } = setup(
|
||||
{
|
||||
@@ -918,6 +966,37 @@ describe("google-meet plugin", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI latest prints the latest conference record", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({}),
|
||||
ensureRuntime: async () => ({}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.parseAsync(
|
||||
[
|
||||
"googlemeet",
|
||||
"latest",
|
||||
"--access-token",
|
||||
"token",
|
||||
"--expires-at",
|
||||
String(Date.now() + 120_000),
|
||||
"--meeting",
|
||||
"abc-defg-hij",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
expect(stdout.output()).toContain("space: spaces/abc-defg-hij");
|
||||
expect(stdout.output()).toContain("conference record: conferenceRecords/rec-1");
|
||||
} finally {
|
||||
stdout.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI artifacts writes markdown output", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const program = new Command();
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
buildGoogleMeetPreflightReport,
|
||||
fetchGoogleMeetArtifacts,
|
||||
fetchGoogleMeetAttendance,
|
||||
fetchLatestGoogleMeetConferenceRecord,
|
||||
fetchGoogleMeetSpace,
|
||||
} from "./src/meet.js";
|
||||
import { handleGoogleMeetNodeHostCommand } from "./src/node-host.js";
|
||||
@@ -150,6 +151,7 @@ const GoogleMeetToolSchema = Type.Object({
|
||||
"setup_status",
|
||||
"resolve_space",
|
||||
"preflight",
|
||||
"latest",
|
||||
"artifacts",
|
||||
"attendance",
|
||||
"recover_current_tab",
|
||||
@@ -388,6 +390,26 @@ export default definePluginEntry({
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"googlemeet.latest",
|
||||
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asParamRecord(params);
|
||||
const meeting = resolveMeetingInput(config, raw.meeting);
|
||||
const token = await resolveGoogleMeetTokenFromParams(config, raw);
|
||||
respond(
|
||||
true,
|
||||
await fetchLatestGoogleMeetConferenceRecord({
|
||||
accessToken: token.accessToken,
|
||||
meeting,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
sendError(respond, err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"googlemeet.artifacts",
|
||||
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
||||
@@ -563,6 +585,16 @@ export default definePluginEntry({
|
||||
}),
|
||||
);
|
||||
}
|
||||
case "latest": {
|
||||
const meeting = resolveMeetingInput(config, raw.meeting);
|
||||
const token = await resolveGoogleMeetTokenFromParams(config, raw);
|
||||
return json(
|
||||
await fetchLatestGoogleMeetConferenceRecord({
|
||||
accessToken: token.accessToken,
|
||||
meeting,
|
||||
}),
|
||||
);
|
||||
}
|
||||
case "artifacts": {
|
||||
const resolved = await resolveArtifactQueryFromParams(config, raw);
|
||||
return json(
|
||||
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
createGoogleMeetSpace,
|
||||
fetchGoogleMeetArtifacts,
|
||||
fetchGoogleMeetAttendance,
|
||||
fetchLatestGoogleMeetConferenceRecord,
|
||||
fetchGoogleMeetSpace,
|
||||
type GoogleMeetArtifactsResult,
|
||||
type GoogleMeetAttendanceResult,
|
||||
type GoogleMeetLatestConferenceRecordResult,
|
||||
} from "./meet.js";
|
||||
import {
|
||||
buildGoogleMeetAuthUrl,
|
||||
@@ -547,6 +549,18 @@ function writeAttendanceSummary(result: GoogleMeetAttendanceResult): void {
|
||||
}
|
||||
}
|
||||
|
||||
function writeLatestConferenceRecordSummary(result: GoogleMeetLatestConferenceRecordResult): void {
|
||||
writeStdoutLine("input: %s", result.input);
|
||||
writeStdoutLine("space: %s", result.space.name);
|
||||
if (!result.conferenceRecord) {
|
||||
writeStdoutLine("conference record: none");
|
||||
return;
|
||||
}
|
||||
writeStdoutLine("conference record: %s", result.conferenceRecord.name);
|
||||
writeStdoutLine("started: %s", formatOptional(result.conferenceRecord.startTime));
|
||||
writeStdoutLine("ended: %s", formatOptional(result.conferenceRecord.endTime));
|
||||
}
|
||||
|
||||
function pushMarkdownLine(lines: string[], text = ""): void {
|
||||
lines.push(text);
|
||||
}
|
||||
@@ -974,6 +988,37 @@ export function registerGoogleMeetCli(params: {
|
||||
}
|
||||
});
|
||||
|
||||
root
|
||||
.command("latest")
|
||||
.description("Find the latest Meet conference record for a meeting")
|
||||
.option("--meeting <value>", "Meet URL, meeting code, or spaces/{id}")
|
||||
.option("--access-token <token>", "Access token override")
|
||||
.option("--refresh-token <token>", "Refresh token override")
|
||||
.option("--client-id <id>", "OAuth client id override")
|
||||
.option("--client-secret <secret>", "OAuth client secret override")
|
||||
.option("--expires-at <ms>", "Cached access token expiry as unix epoch milliseconds")
|
||||
.option("--json", "Print JSON output", false)
|
||||
.action(async (options: ResolveSpaceOptions) => {
|
||||
const resolved = resolveTokenOptions(params.config, options);
|
||||
const token = await resolveGoogleMeetAccessToken(resolved);
|
||||
const result = await fetchLatestGoogleMeetConferenceRecord({
|
||||
accessToken: token.accessToken,
|
||||
meeting: resolved.meeting,
|
||||
});
|
||||
if (options.json) {
|
||||
writeStdoutJson({
|
||||
...result,
|
||||
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
});
|
||||
return;
|
||||
}
|
||||
writeLatestConferenceRecordSummary(result);
|
||||
writeStdoutLine(
|
||||
"token source: %s",
|
||||
token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
);
|
||||
});
|
||||
|
||||
root
|
||||
.command("artifacts")
|
||||
.description("List Meet conference records and available participant/artifact metadata")
|
||||
|
||||
@@ -112,6 +112,12 @@ export type GoogleMeetArtifactsResult = {
|
||||
artifacts: GoogleMeetArtifactsEntry[];
|
||||
};
|
||||
|
||||
export type GoogleMeetLatestConferenceRecordResult = {
|
||||
input: string;
|
||||
space: GoogleMeetSpace;
|
||||
conferenceRecord?: GoogleMeetConferenceRecord;
|
||||
};
|
||||
|
||||
export type GoogleMeetAttendanceRow = {
|
||||
conferenceRecord: string;
|
||||
participant: string;
|
||||
@@ -257,6 +263,7 @@ async function listGoogleMeetCollection<T extends { name?: string }>(params: {
|
||||
path: string;
|
||||
collectionKey: string;
|
||||
query?: Record<string, string | number | boolean | undefined>;
|
||||
maxItems?: number;
|
||||
auditContext: string;
|
||||
errorPrefix: string;
|
||||
}): Promise<T[]> {
|
||||
@@ -270,13 +277,17 @@ async function listGoogleMeetCollection<T extends { name?: string }>(params: {
|
||||
auditContext: params.auditContext,
|
||||
errorPrefix: params.errorPrefix,
|
||||
});
|
||||
items.push(
|
||||
...assertResourceArray<T>(
|
||||
payload[params.collectionKey],
|
||||
params.collectionKey,
|
||||
params.errorPrefix,
|
||||
),
|
||||
const pageItems = assertResourceArray<T>(
|
||||
payload[params.collectionKey],
|
||||
params.collectionKey,
|
||||
params.errorPrefix,
|
||||
);
|
||||
const remaining =
|
||||
typeof params.maxItems === "number" ? Math.max(params.maxItems - items.length, 0) : undefined;
|
||||
items.push(...(remaining === undefined ? pageItems : pageItems.slice(0, remaining)));
|
||||
if (typeof params.maxItems === "number" && items.length >= params.maxItems) {
|
||||
break;
|
||||
}
|
||||
pageToken = typeof payload.nextPageToken === "string" ? payload.nextPageToken : undefined;
|
||||
} while (pageToken);
|
||||
return items;
|
||||
@@ -370,6 +381,7 @@ export async function listGoogleMeetConferenceRecords(params: {
|
||||
accessToken: string;
|
||||
meeting?: string;
|
||||
pageSize?: number;
|
||||
maxItems?: number;
|
||||
}): Promise<GoogleMeetConferenceRecord[]> {
|
||||
const filter = params.meeting
|
||||
? `space.name = "${normalizeGoogleMeetSpaceName(params.meeting)}"`
|
||||
@@ -382,11 +394,33 @@ export async function listGoogleMeetConferenceRecords(params: {
|
||||
pageSize: params.pageSize,
|
||||
filter,
|
||||
},
|
||||
maxItems: params.maxItems,
|
||||
auditContext: "google-meet.conferenceRecords.list",
|
||||
errorPrefix: "Google Meet conferenceRecords.list",
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchLatestGoogleMeetConferenceRecord(params: {
|
||||
accessToken: string;
|
||||
meeting: string;
|
||||
}): Promise<GoogleMeetLatestConferenceRecordResult> {
|
||||
const space = await fetchGoogleMeetSpace({
|
||||
accessToken: params.accessToken,
|
||||
meeting: params.meeting,
|
||||
});
|
||||
const [conferenceRecord] = await listGoogleMeetConferenceRecords({
|
||||
accessToken: params.accessToken,
|
||||
meeting: space.name,
|
||||
pageSize: 1,
|
||||
maxItems: 1,
|
||||
});
|
||||
return {
|
||||
input: params.meeting,
|
||||
space,
|
||||
...(conferenceRecord ? { conferenceRecord } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function listGoogleMeetParticipants(params: {
|
||||
accessToken: string;
|
||||
conferenceRecord: string;
|
||||
|
||||
Reference in New Issue
Block a user