From 825da7fd52a28ba6113a9a3cf76f42450f6eda8d Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 19 Feb 2026 01:50:15 +0800 Subject: [PATCH] fix(fal-video): sanitize credentials and urls for kling requests --- lib/ai/providers/fal-video.ts | 44 ++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/lib/ai/providers/fal-video.ts b/lib/ai/providers/fal-video.ts index 7b7476e3a..7bb30e5e7 100644 --- a/lib/ai/providers/fal-video.ts +++ b/lib/ai/providers/fal-video.ts @@ -22,14 +22,34 @@ const DEFAULT_RATIO = "16:9"; const DEFAULT_SHOT_TYPE = "customize"; const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; +function normalizeFalTextInput(value: string) { + return String(value || "") + .replace(/\u00a0/g, " ") + .trim() + .normalize("NFKC"); +} + function normalizeFalCredentials(apiKey: string) { - const normalized = apiKey.replace(/\u00a0/g, " ").trim(); + const normalized = normalizeFalTextInput(apiKey); const stripped = normalized.replace(/^bearer\s+/i, "").replace(/^key\s+/i, ""); const kvMatch = stripped.match(/^(?:fal[_-]?key|api[_-]?key|token)\s*[::]\s*(.+)$/i); if (kvMatch?.[1]) { - return kvMatch[1].trim(); + return kvMatch[1].trim().replace(/[^\x20-\x7E]/g, ""); + } + return stripped.replace(/[^\x20-\x7E]/g, ""); +} + +function normalizeFalModelId(modelId: string) { + return normalizeFalTextInput(modelId); +} + +function normalizeFalImageUrl(url: string) { + const normalized = normalizeFalTextInput(url); + try { + return new URL(normalized).toString(); + } catch { + return normalized; } - return stripped; } function normalizeDuration(value?: number) { @@ -60,7 +80,7 @@ function extractPromptAndImages(messages: Array<{ role: string; content: any }>) const pushImageUrl = (url?: string) => { if (!url) return; - const normalized = String(url).trim(); + const normalized = normalizeFalImageUrl(String(url)); if (normalized.startsWith("data:")) { throw new Error("FAL 视频模型不支持 Base64 图片,请先上传图片后再生成视频"); @@ -212,7 +232,7 @@ export function createFalVideo({ apiKey, baseURL }: FalVideoConfig) { overrideModelId?: string; }) => { try { - const activeModelId = options.overrideModelId || modelId; + const activeModelId = normalizeFalModelId(options.overrideModelId || modelId); const { prompt, imageUrls } = extractPromptAndImages(options.messages); const input = buildFalInput({ prompt, @@ -229,8 +249,12 @@ export function createFalVideo({ apiKey, baseURL }: FalVideoConfig) { }); const fal = await import("@fal-ai/serverless-client"); + const credentials = normalizeFalCredentials(apiKey); + if (!credentials) { + throw new Error("FAL API Key 未配置或格式无效"); + } fal.config({ - credentials: normalizeFalCredentials(apiKey), + credentials, }); const result: any = await fal.subscribe(activeModelId, { @@ -266,8 +290,12 @@ export function createFalVideo({ apiKey, baseURL }: FalVideoConfig) { }; } catch (error) { console.error("[Fal Video] Generation failed", error); - const errorText = - error instanceof Error ? `视频生成失败: ${error.message}` : "视频生成失败,请稍后重试"; + const rawErrorText = error instanceof Error ? error.message : ""; + const errorText = /ByteString/i.test(rawErrorText) + ? "视频生成失败: 检测到配置中包含非法字符(常见为中文冒号),请检查 FAL API Key 与模型ID" + : error instanceof Error + ? `视频生成失败: ${error.message}` + : "视频生成失败,请稍后重试"; return { text: errorText, finishReason: "error" as const,