mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:40:44 +00:00
feat(google-meet): export artifacts reports
This commit is contained in:
@@ -75,6 +75,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/Google Meet: default Chrome realtime sessions to OpenAI plus SoX `rec`/`play` audio bridge commands, so the usual setup only needs the plugin enabled and `OPENAI_API_KEY`. Thanks @steipete.
|
||||
- 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 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.
|
||||
|
||||
@@ -642,6 +642,15 @@ openclaw googlemeet artifacts --conference-record conferenceRecords/abc123 --jso
|
||||
openclaw googlemeet attendance --conference-record conferenceRecords/abc123 --json
|
||||
```
|
||||
|
||||
Write a readable report:
|
||||
|
||||
```bash
|
||||
openclaw googlemeet artifacts --conference-record conferenceRecords/abc123 \
|
||||
--format markdown --output meet-artifacts.md
|
||||
openclaw googlemeet attendance --conference-record conferenceRecords/abc123 \
|
||||
--format markdown --output meet-attendance.md
|
||||
```
|
||||
|
||||
`artifacts` returns conference record metadata plus participant, recording,
|
||||
transcript, structured transcript-entry, and smart-note resource metadata when
|
||||
Google exposes it for the meeting. Use `--no-transcript-entries` to skip
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { PassThrough, Writable } from "node:stream";
|
||||
import { Command } from "commander";
|
||||
import type { RealtimeVoiceProviderPlugin } from "openclaw/plugin-sdk/realtime-voice";
|
||||
@@ -915,6 +918,48 @@ describe("google-meet plugin", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI artifacts writes markdown output", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), "openclaw-google-meet-artifacts-"));
|
||||
const outputPath = path.join(tempDir, "artifacts.md");
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({}),
|
||||
ensureRuntime: async () => ({}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.parseAsync(
|
||||
[
|
||||
"googlemeet",
|
||||
"artifacts",
|
||||
"--access-token",
|
||||
"token",
|
||||
"--expires-at",
|
||||
String(Date.now() + 120_000),
|
||||
"--conference-record",
|
||||
"rec-1",
|
||||
"--format",
|
||||
"markdown",
|
||||
"--output",
|
||||
outputPath,
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
const markdown = readFileSync(outputPath, "utf8");
|
||||
expect(stdout.output()).toContain(`wrote: ${outputPath}`);
|
||||
expect(markdown).toContain("# Google Meet Artifacts");
|
||||
expect(markdown).toContain("## conferenceRecords/rec-1");
|
||||
expect(markdown).toContain("### Transcript Entries: conferenceRecords/rec-1/transcripts/t1");
|
||||
expect(markdown).toContain("Hello from the transcript.");
|
||||
} finally {
|
||||
stdout.restore();
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI attendance prints participant sessions by default", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const program = new Command();
|
||||
@@ -949,6 +994,42 @@ describe("google-meet plugin", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI attendance prints markdown output", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({}),
|
||||
ensureRuntime: async () => ({}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.parseAsync(
|
||||
[
|
||||
"googlemeet",
|
||||
"attendance",
|
||||
"--access-token",
|
||||
"token",
|
||||
"--expires-at",
|
||||
String(Date.now() + 120_000),
|
||||
"--conference-record",
|
||||
"rec-1",
|
||||
"--format",
|
||||
"markdown",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
expect(stdout.output()).toContain("# Google Meet Attendance");
|
||||
expect(stdout.output()).toContain("## Alice");
|
||||
expect(stdout.output()).toContain(
|
||||
"conferenceRecords/rec-1/participants/p1/participantSessions/s1",
|
||||
);
|
||||
} finally {
|
||||
stdout.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI doctor prints human-readable session health", async () => {
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { format } from "node:util";
|
||||
import type { Command } from "commander";
|
||||
@@ -52,6 +53,8 @@ type MeetArtifactOptions = ResolveSpaceOptions & {
|
||||
conferenceRecord?: string;
|
||||
pageSize?: string;
|
||||
transcriptEntries?: boolean;
|
||||
format?: "summary" | "markdown";
|
||||
output?: string;
|
||||
};
|
||||
|
||||
type SetupOptions = {
|
||||
@@ -98,6 +101,15 @@ function writeStdoutLine(...values: unknown[]): void {
|
||||
process.stdout.write(`${format(...values)}\n`);
|
||||
}
|
||||
|
||||
async function writeCliOutput(options: { output?: string }, text: string): Promise<void> {
|
||||
if (options.output?.trim()) {
|
||||
await writeFile(options.output, text.endsWith("\n") ? text : `${text}\n`, "utf8");
|
||||
writeStdoutLine("wrote: %s", options.output);
|
||||
return;
|
||||
}
|
||||
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
||||
}
|
||||
|
||||
async function promptInput(message: string): Promise<string> {
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
@@ -535,6 +547,123 @@ function writeAttendanceSummary(result: GoogleMeetAttendanceResult): void {
|
||||
}
|
||||
}
|
||||
|
||||
function pushMarkdownLine(lines: string[], text = ""): void {
|
||||
lines.push(text);
|
||||
}
|
||||
|
||||
function formatMarkdownOptional(value: unknown): string {
|
||||
return typeof value === "string" && value.trim() ? value : "n/a";
|
||||
}
|
||||
|
||||
function formatMarkdownIdentity(row: GoogleMeetAttendanceResult["attendance"][number]): string {
|
||||
return row.displayName || row.user || row.participant;
|
||||
}
|
||||
|
||||
function renderArtifactsMarkdown(result: GoogleMeetArtifactsResult): string {
|
||||
const lines: string[] = ["# Google Meet Artifacts"];
|
||||
if (result.input) {
|
||||
pushMarkdownLine(lines, `Input: ${result.input}`);
|
||||
}
|
||||
if (result.space) {
|
||||
pushMarkdownLine(lines, `Space: ${result.space.name}`);
|
||||
}
|
||||
pushMarkdownLine(lines);
|
||||
pushMarkdownLine(lines, `Conference records: ${result.conferenceRecords.length}`);
|
||||
for (const entry of result.artifacts) {
|
||||
pushMarkdownLine(lines);
|
||||
pushMarkdownLine(lines, `## ${entry.conferenceRecord.name}`);
|
||||
pushMarkdownLine(lines, `Started: ${formatMarkdownOptional(entry.conferenceRecord.startTime)}`);
|
||||
pushMarkdownLine(lines, `Ended: ${formatMarkdownOptional(entry.conferenceRecord.endTime)}`);
|
||||
pushMarkdownLine(lines);
|
||||
pushMarkdownLine(lines, `Participants: ${entry.participants.length}`);
|
||||
pushMarkdownLine(lines, `Recordings: ${entry.recordings.length}`);
|
||||
pushMarkdownLine(lines, `Transcripts: ${entry.transcripts.length}`);
|
||||
pushMarkdownLine(
|
||||
lines,
|
||||
`Transcript entries: ${entry.transcriptEntries.reduce(
|
||||
(count, transcript) => count + transcript.entries.length,
|
||||
0,
|
||||
)}`,
|
||||
);
|
||||
pushMarkdownLine(lines, `Smart notes: ${entry.smartNotes.length}`);
|
||||
if (entry.recordings.length > 0) {
|
||||
pushMarkdownLine(lines);
|
||||
pushMarkdownLine(lines, "### Recordings");
|
||||
for (const recording of entry.recordings) {
|
||||
pushMarkdownLine(lines, `- ${recording.name}`);
|
||||
}
|
||||
}
|
||||
if (entry.transcripts.length > 0) {
|
||||
pushMarkdownLine(lines);
|
||||
pushMarkdownLine(lines, "### Transcripts");
|
||||
for (const transcript of entry.transcripts) {
|
||||
pushMarkdownLine(lines, `- ${transcript.name}`);
|
||||
}
|
||||
}
|
||||
for (const transcriptEntries of entry.transcriptEntries) {
|
||||
pushMarkdownLine(lines);
|
||||
pushMarkdownLine(lines, `### Transcript Entries: ${transcriptEntries.transcript}`);
|
||||
if (transcriptEntries.entriesError) {
|
||||
pushMarkdownLine(lines, `Warning: ${transcriptEntries.entriesError}`);
|
||||
continue;
|
||||
}
|
||||
if (transcriptEntries.entries.length === 0) {
|
||||
pushMarkdownLine(lines, "_No transcript entries._");
|
||||
continue;
|
||||
}
|
||||
for (const transcriptEntry of transcriptEntries.entries) {
|
||||
const times =
|
||||
transcriptEntry.startTime || transcriptEntry.endTime
|
||||
? ` (${formatMarkdownOptional(transcriptEntry.startTime)} -> ${formatMarkdownOptional(
|
||||
transcriptEntry.endTime,
|
||||
)})`
|
||||
: "";
|
||||
const speaker = transcriptEntry.participant ? `${transcriptEntry.participant}: ` : "";
|
||||
pushMarkdownLine(lines, `- ${speaker}${transcriptEntry.text ?? ""}${times}`);
|
||||
}
|
||||
}
|
||||
if (entry.smartNotes.length > 0) {
|
||||
pushMarkdownLine(lines);
|
||||
pushMarkdownLine(lines, "### Smart Notes");
|
||||
for (const smartNote of entry.smartNotes) {
|
||||
pushMarkdownLine(lines, `- ${smartNote.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
function renderAttendanceMarkdown(result: GoogleMeetAttendanceResult): string {
|
||||
const lines: string[] = ["# Google Meet Attendance"];
|
||||
if (result.input) {
|
||||
pushMarkdownLine(lines, `Input: ${result.input}`);
|
||||
}
|
||||
if (result.space) {
|
||||
pushMarkdownLine(lines, `Space: ${result.space.name}`);
|
||||
}
|
||||
pushMarkdownLine(lines);
|
||||
pushMarkdownLine(lines, `Conference records: ${result.conferenceRecords.length}`);
|
||||
pushMarkdownLine(lines, `Attendance rows: ${result.attendance.length}`);
|
||||
for (const row of result.attendance) {
|
||||
pushMarkdownLine(lines);
|
||||
pushMarkdownLine(lines, `## ${formatMarkdownIdentity(row)}`);
|
||||
pushMarkdownLine(lines, `Record: ${row.conferenceRecord}`);
|
||||
pushMarkdownLine(lines, `Resource: ${row.participant}`);
|
||||
pushMarkdownLine(lines, `First joined: ${formatMarkdownOptional(row.earliestStartTime)}`);
|
||||
pushMarkdownLine(lines, `Last left: ${formatMarkdownOptional(row.latestEndTime)}`);
|
||||
pushMarkdownLine(lines, `Sessions: ${row.sessions.length}`);
|
||||
for (const session of row.sessions) {
|
||||
pushMarkdownLine(
|
||||
lines,
|
||||
`- ${session.name}: ${formatMarkdownOptional(session.startTime)} -> ${formatMarkdownOptional(
|
||||
session.endTime,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
export function registerGoogleMeetCli(params: {
|
||||
program: Command;
|
||||
config: GoogleMeetConfig;
|
||||
@@ -857,6 +986,8 @@ export function registerGoogleMeetCli(params: {
|
||||
.option("--expires-at <ms>", "Cached access token expiry as unix epoch milliseconds")
|
||||
.option("--page-size <n>", "Max resources per Meet API page")
|
||||
.option("--no-transcript-entries", "Skip structured transcript entry lookup")
|
||||
.option("--format <format>", "Output format: summary or markdown", "summary")
|
||||
.option("--output <path>", "Write output to a file instead of stdout")
|
||||
.option("--json", "Print JSON output", false)
|
||||
.action(async (options: MeetArtifactOptions) => {
|
||||
const resolved = resolveArtifactTokenOptions(params.config, options);
|
||||
@@ -869,12 +1000,26 @@ export function registerGoogleMeetCli(params: {
|
||||
includeTranscriptEntries: resolved.includeTranscriptEntries,
|
||||
});
|
||||
if (options.json) {
|
||||
writeStdoutJson({
|
||||
...result,
|
||||
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
});
|
||||
await writeCliOutput(
|
||||
options,
|
||||
JSON.stringify(
|
||||
{
|
||||
...result,
|
||||
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (options.format === "markdown") {
|
||||
await writeCliOutput(options, renderArtifactsMarkdown(result));
|
||||
return;
|
||||
}
|
||||
if (options.format && options.format !== "summary") {
|
||||
throw new Error("Unsupported format. Expected summary or markdown.");
|
||||
}
|
||||
writeArtifactsSummary(result);
|
||||
writeStdoutLine(
|
||||
"token source: %s",
|
||||
@@ -893,6 +1038,8 @@ export function registerGoogleMeetCli(params: {
|
||||
.option("--client-secret <secret>", "OAuth client secret override")
|
||||
.option("--expires-at <ms>", "Cached access token expiry as unix epoch milliseconds")
|
||||
.option("--page-size <n>", "Max resources per Meet API page")
|
||||
.option("--format <format>", "Output format: summary or markdown", "summary")
|
||||
.option("--output <path>", "Write output to a file instead of stdout")
|
||||
.option("--json", "Print JSON output", false)
|
||||
.action(async (options: MeetArtifactOptions) => {
|
||||
const resolved = resolveArtifactTokenOptions(params.config, options);
|
||||
@@ -904,12 +1051,26 @@ export function registerGoogleMeetCli(params: {
|
||||
pageSize: resolved.pageSize,
|
||||
});
|
||||
if (options.json) {
|
||||
writeStdoutJson({
|
||||
...result,
|
||||
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
});
|
||||
await writeCliOutput(
|
||||
options,
|
||||
JSON.stringify(
|
||||
{
|
||||
...result,
|
||||
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (options.format === "markdown") {
|
||||
await writeCliOutput(options, renderAttendanceMarkdown(result));
|
||||
return;
|
||||
}
|
||||
if (options.format && options.format !== "summary") {
|
||||
throw new Error("Unsupported format. Expected summary or markdown.");
|
||||
}
|
||||
writeAttendanceSummary(result);
|
||||
writeStdoutLine(
|
||||
"token source: %s",
|
||||
|
||||
Reference in New Issue
Block a user