refactor(google-meet): use JSZip for exports

This commit is contained in:
Peter Steinberger
2026-07-14 11:27:29 +01:00
parent 276efaa48b
commit 5c509efb02
4 changed files with 13 additions and 89 deletions

View File

@@ -8,6 +8,7 @@
},
"type": "module",
"dependencies": {
"jszip": "3.10.1",
"pretty-ms": "9.3.0",
"typebox": "1.3.3"
},

View File

@@ -3,6 +3,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { Command } from "commander";
import JSZip from "jszip";
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { registerGoogleMeetCli, testing } from "./cli.js";
@@ -674,7 +675,8 @@ describe("google-meet CLI", () => {
expectFields(firstRecord(firstRecord(artifacts.artifacts).transcripts), {
documentText: "Transcript document body.",
});
expect(readFileSync(`${tempDir}.zip`).subarray(0, 4).toString("hex")).toBe("504b0304");
const zip = await JSZip.loadAsync(readFileSync(`${tempDir}.zip`));
expect(await zip.file("summary.md")?.async("string")).toContain("# Google Meet Artifacts");
} finally {
stdout.restore();
rmSync(tempDir, { recursive: true, force: true });

View File

@@ -3,8 +3,8 @@ import path from "node:path";
import { createInterface } from "node:readline/promises";
import { format } from "node:util";
import type { Command } from "commander";
import JSZip from "jszip";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime";
import {
clampTimerTimeoutMs,
@@ -1302,92 +1302,6 @@ function defaultExportDirectory(): string {
return `google-meet-export-${new Date().toISOString().replace(/[:.]/g, "-")}`;
}
const CRC32_TABLE = new Uint32Array(
Array.from({ length: 256 }, (_, index) => {
let value = index;
for (let bit = 0; bit < 8; bit += 1) {
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
}
return value >>> 0;
}),
);
function crc32(buffer: Buffer): number {
let value = 0xffffffff;
for (const byte of buffer) {
const tableValue = expectDefined(
CRC32_TABLE.at((value ^ byte) & 0xff),
"CRC32 lookup table entry",
);
value = tableValue ^ (value >>> 8);
}
return (value ^ 0xffffffff) >>> 0;
}
function dosDateTime(date = new Date()): { date: number; time: number } {
return {
time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
date: ((date.getFullYear() - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
};
}
function buildZipArchive(files: Array<{ name: string; content: string }>): Buffer {
const localParts: Buffer[] = [];
const centralParts: Buffer[] = [];
let offset = 0;
const stamp = dosDateTime();
for (const file of files) {
const name = Buffer.from(file.name, "utf8");
const content = Buffer.from(file.content, "utf8");
const checksum = crc32(content);
const local = Buffer.alloc(30);
local.writeUInt32LE(0x04034b50, 0);
local.writeUInt16LE(20, 4);
local.writeUInt16LE(0, 6);
local.writeUInt16LE(0, 8);
local.writeUInt16LE(stamp.time, 10);
local.writeUInt16LE(stamp.date, 12);
local.writeUInt32LE(checksum, 14);
local.writeUInt32LE(content.length, 18);
local.writeUInt32LE(content.length, 22);
local.writeUInt16LE(name.length, 26);
local.writeUInt16LE(0, 28);
localParts.push(local, name, content);
const central = Buffer.alloc(46);
central.writeUInt32LE(0x02014b50, 0);
central.writeUInt16LE(20, 4);
central.writeUInt16LE(20, 6);
central.writeUInt16LE(0, 8);
central.writeUInt16LE(0, 10);
central.writeUInt16LE(stamp.time, 12);
central.writeUInt16LE(stamp.date, 14);
central.writeUInt32LE(checksum, 16);
central.writeUInt32LE(content.length, 20);
central.writeUInt32LE(content.length, 24);
central.writeUInt16LE(name.length, 28);
central.writeUInt16LE(0, 30);
central.writeUInt16LE(0, 32);
central.writeUInt16LE(0, 34);
central.writeUInt16LE(0, 36);
central.writeUInt32LE(0, 38);
central.writeUInt32LE(offset, 42);
centralParts.push(central, name);
offset += local.length + name.length + content.length;
}
const centralDirectory = Buffer.concat(centralParts);
const end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50, 0);
end.writeUInt16LE(0, 4);
end.writeUInt16LE(0, 6);
end.writeUInt16LE(files.length, 8);
end.writeUInt16LE(files.length, 10);
end.writeUInt32LE(centralDirectory.length, 12);
end.writeUInt32LE(offset, 16);
end.writeUInt16LE(0, 20);
return Buffer.concat([...localParts, centralDirectory, end]);
}
export async function writeMeetExportBundle(params: {
outputDir?: string;
artifacts: GoogleMeetArtifactsResult;
@@ -1435,7 +1349,11 @@ export async function writeMeetExportBundle(params: {
files: files.map((file) => path.join(outputDir, file.name)),
};
if (zipFile) {
await writeFile(zipFile, buildZipArchive(files));
const zip = new JSZip();
for (const file of files) {
zip.file(file.name, file.content);
}
await writeFile(zipFile, await zip.generateAsync({ type: "nodebuffer" }));
result.zipFile = zipFile;
}
return result;

3
pnpm-lock.yaml generated
View File

@@ -853,6 +853,9 @@ importers:
extensions/google-meet:
dependencies:
jszip:
specifier: 3.10.1
version: 3.10.1
pretty-ms:
specifier: 9.3.0
version: 9.3.0