




















1-import { readFile } from "node:fs/promises";
2-import path from "node:path";
31import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
42import {
53createProviderOperationDeadline,
64executeProviderOperationWithRetry,
75resolveProviderOperationTimeoutMs,
86waitProviderOperationPollInterval,
97} from "openclaw/plugin-sdk/provider-http";
10-import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
8+import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
119import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
1210import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
13-import { resolvePreferredOpenClawTmpDir, withTempWorkspace } from "openclaw/plugin-sdk/temp-path";
1411import type {
1512GeneratedVideoAsset,
1613VideoGenerationProvider,
@@ -29,6 +26,7 @@ import { createGoogleGenAI, type GoogleGenAIClient } from "./google-genai-runtim
2926const DEFAULT_TIMEOUT_MS = 180_000;
3027const POLL_INTERVAL_MS = 10_000;
3128const MAX_POLL_ATTEMPTS = 120;
29+const DEFAULT_GENERATED_VIDEO_MAX_BYTES = 16 * 1024 * 1024;
3230const GOOGLE_VIDEO_EMPTY_RESULT_MESSAGE =
3331"Google video generation response missing generated videos";
3432@@ -37,6 +35,20 @@ function resolveConfiguredGoogleVideoBaseUrl(req: VideoGenerationRequest): strin
3735return configured ? resolveGoogleGenerativeAiApiOrigin(configured) : undefined;
3836}
393738+function resolveGeneratedVideoMaxBytes(req: VideoGenerationRequest): number {
39+const configured = req.cfg.agents?.defaults?.mediaMaxMb;
40+if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {
41+return Math.floor(configured * 1024 * 1024);
42+}
43+return DEFAULT_GENERATED_VIDEO_MAX_BYTES;
44+}
45+46+function assertGeneratedVideoBufferWithinLimit(buffer: Buffer, maxBytes: number): void {
47+if (buffer.length > maxBytes) {
48+throw new Error(`Google generated video download exceeds ${maxBytes} bytes`);
49+}
50+}
51+4052function resolveGoogleVideoRestBaseUrl(configuredBaseUrl?: string): string {
4153return `${configuredBaseUrl ?? "https://generativelanguage.googleapis.com"}/v1beta`;
4254}
@@ -148,42 +160,6 @@ function resolveInputVideo(req: VideoGenerationRequest) {
148160};
149161}
150162151-async function downloadGeneratedVideo(params: {
152-client: GoogleGenAIClient;
153-file: unknown;
154-index: number;
155-}): Promise<GeneratedVideoAsset> {
156-return await withTempWorkspace(
157-{ rootDir: resolvePreferredOpenClawTmpDir(), prefix: "openclaw-google-video-" },
158-async ({ dir: tempDir }) => {
159-const fileName = `video-${params.index + 1}.mp4`;
160-const downloadPath = path.join(tempDir, fileName);
161-await writeExternalFileWithinRoot({
162-rootDir: tempDir,
163-path: fileName,
164-write: async (downloadPath) => {
165-await executeProviderOperationWithRetry({
166-provider: "google",
167-stage: "download",
168-operation: async () => {
169-await params.client.files.download({
170-file: params.file as never,
171- downloadPath,
172-});
173-},
174-});
175-},
176-});
177-const buffer = await readFile(downloadPath);
178-return {
179- buffer,
180-mimeType: "video/mp4",
181-fileName: `video-${params.index + 1}.mp4`,
182-};
183-},
184-);
185-}
186-187163function resolveGoogleGeneratedVideoDownloadUrl(params: {
188164uri: string | undefined;
189165apiKey: string;
@@ -222,12 +198,31 @@ function resolveGoogleGeneratedVideoDownloadUrl(params: {
222198return url.toString();
223199}
224200201+function resolveGoogleGeneratedVideoFileDownloadUrl(params: {
202+file: unknown;
203+apiKey: string;
204+configuredBaseUrl?: string;
205+}): string | undefined {
206+const resource = params.file as { name?: unknown; uri?: unknown } | undefined;
207+const name = normalizeOptionalString(resource?.name) ?? normalizeOptionalString(resource?.uri);
208+if (!name || !/^files\/[^/?#]+$/u.test(name)) {
209+return undefined;
210+}
211+const baseUrl = resolveGoogleVideoRestBaseUrl(params.configuredBaseUrl);
212+const url = new URL(`${baseUrl}/${name}:download`);
213+url.searchParams.set("alt", "media");
214+url.searchParams.set("key", params.apiKey);
215+return url.toString();
216+}
217+225218async function downloadGeneratedVideoFromUri(params: {
226219uri: string | undefined;
227220apiKey: string;
228221configuredBaseUrl?: string;
229222mimeType?: string;
230223index: number;
224+maxBytes: number;
225+timeoutMs: number;
231226}): Promise<GeneratedVideoAsset | undefined> {
232227const downloadUrl = resolveGoogleGeneratedVideoDownloadUrl({
233228uri: params.uri,
@@ -243,14 +238,21 @@ async function downloadGeneratedVideoFromUri(params: {
243238operation: async () => {
244239const { response, release } = await fetchWithSsrFGuard({
245240url: downloadUrl,
241+timeoutMs: params.timeoutMs,
246242});
247243try {
248244if (!response.ok) {
249245throw new Error(
250246`Failed to download Google generated video: ${response.status} ${response.statusText}`,
251247);
252248}
253-const buffer = Buffer.from(await response.arrayBuffer());
249+const buffer = await readResponseWithLimit(response, params.maxBytes, {
250+chunkTimeoutMs: params.timeoutMs,
251+onOverflow: ({ maxBytes }) =>
252+new Error(`Google generated video download exceeds ${maxBytes} bytes`),
253+onIdleTimeout: ({ chunkTimeoutMs }) =>
254+new Error(`Google generated video download stalled after ${chunkTimeoutMs}ms`),
255+});
254256return {
255257 buffer,
256258mimeType:
@@ -545,14 +547,17 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
545547if (generatedVideos.length === 0) {
546548throw new Error(GOOGLE_VIDEO_EMPTY_RESULT_MESSAGE);
547549}
550+const maxVideoBytes = resolveGeneratedVideoMaxBytes(req);
548551const videos = await Promise.all(
549552generatedVideos.map(async (entry, index) => {
550553const inline = entry.video as
551554| { videoBytes?: string; uri?: string; mimeType?: string }
552555| undefined;
553556if (inline?.videoBytes) {
557+const buffer = Buffer.from(inline.videoBytes, "base64");
558+assertGeneratedVideoBufferWithinLimit(buffer, maxVideoBytes);
554559return {
555-buffer: Buffer.from(inline.videoBytes, "base64"),
560+ buffer,
556561mimeType: normalizeOptionalString(inline.mimeType) || "video/mp4",
557562fileName: `video-${index + 1}.mp4`,
558563};
@@ -563,18 +568,38 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
563568 configuredBaseUrl,
564569mimeType: inline?.mimeType,
565570 index,
571+maxBytes: maxVideoBytes,
572+timeoutMs: resolveProviderOperationTimeoutMs({
573+ deadline,
574+defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
575+}),
566576});
567577if (directDownload) {
568578return directDownload;
569579}
570580if (!inline) {
571581throw new Error("Google generated video missing file handle");
572582}
573-return await downloadGeneratedVideo({
574- client,
575-file: inline,
583+const fileDownload = await downloadGeneratedVideoFromUri({
584+uri: resolveGoogleGeneratedVideoFileDownloadUrl({
585+file: inline,
586+ apiKey,
587+ configuredBaseUrl,
588+}),
589+ apiKey,
590+ configuredBaseUrl,
591+mimeType: inline.mimeType,
576592 index,
593+maxBytes: maxVideoBytes,
594+timeoutMs: resolveProviderOperationTimeoutMs({
595+ deadline,
596+defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
597+}),
577598});
599+if (!fileDownload) {
600+throw new Error("Google generated video missing bounded download URL");
601+}
602+return fileDownload;
578603}),
579604);
580605return {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。