diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 606be27..af6a54a 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -178,6 +178,55 @@ function injectHistoryImagesToLastUserMessage( * 因为这些 provider 内部会自行处理图片上传到 S3 */ async function convertMessagesWithImages(messages: ChatMessage[], providerType: string) { + if (providerType === "ruxa-image") { + const convertedMessages = []; + + for (const message of messages) { + const content: any[] = []; + + for (const part of message.parts) { + const partAny = part as any; + + if (partAny.type === "text" && partAny.text) { + content.push({ + type: "text", + text: partAny.text, + }); + continue; + } + + const candidateUrl = + partAny.type === "file" + ? (partAny.url || partAny.data) + : partAny.type === "image" + ? (partAny.image || partAny.url) + : partAny.type === "image_url" + ? partAny.image_url?.url + : undefined; + + if (!candidateUrl || typeof candidateUrl !== "string") continue; + if (candidateUrl.startsWith("data:")) { + throw new Error("RUXA 不支持 Base64 图片传输,请先上传图片后再编辑"); + } + if (!candidateUrl.startsWith("http")) continue; + + content.push({ + type: "image_url", + image_url: { + url: candidateUrl, + }, + }); + } + + convertedMessages.push({ + role: message.role, + content: content.length === 1 && content[0].type === "text" ? content[0].text : content, + }); + } + + return convertedMessages; + } + // 所有 provider 都使用 URL 格式传输图片 // gemini-image 和 huanke-gemini 内部会将 Base64 转换为 S3 URL // 这里不再进行 Base64 转换,直接返回标准格式 @@ -1252,7 +1301,9 @@ export async function POST(request: Request) { const tryGenerateImage = async (modelIdToUse: string, isRetry: boolean) => { // 转换消息(阿里生图支持 URL,会自动上传 base64) - const modelMessages = await convertMessagesWithImages(messagesForAlibaba, "alibaba-image"); + const providerForConvert = + (languageModel as any).provider === "ruxa-image" ? "ruxa-image" : "alibaba-image"; + const modelMessages = await convertMessagesWithImages(messagesForAlibaba, providerForConvert); // 调用 doGenerate 获取图片,传递模型显示名称 const result = await (languageModel as any).doGenerate({ diff --git a/lib/ai/providers/ruxa-image.ts b/lib/ai/providers/ruxa-image.ts index 0214eda..8fc706c 100644 --- a/lib/ai/providers/ruxa-image.ts +++ b/lib/ai/providers/ruxa-image.ts @@ -20,8 +20,8 @@ interface UploadedImage { original: string; } -const DEFAULT_ASPECT_RATIO = "1:1"; -const DEFAULT_RESOLUTION = "1K"; +const DEFAULT_ASPECT_RATIO = "auto"; +const DEFAULT_RESOLUTION = "1k"; const DEFAULT_OUTPUT_FORMAT = "png"; const POLL_INTERVAL_MS = 2000; const POLL_MAX_ATTEMPTS = 60; @@ -50,17 +50,30 @@ function joinApiPath(baseURL: string, path: string) { } function normalizeAspectRatio(aspectRatio?: string) { - if (!aspectRatio || aspectRatio === "auto") return DEFAULT_ASPECT_RATIO; - return aspectRatio; + if (!aspectRatio) return DEFAULT_ASPECT_RATIO; + const normalized = aspectRatio.trim().toLowerCase(); + if (normalized === "auto") return "auto"; + const ratio = aspectRatio.trim(); + if (["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"].includes(ratio)) { + return ratio; + } + return DEFAULT_ASPECT_RATIO; } function normalizeResolution(imageSize?: string) { if (!imageSize) return DEFAULT_RESOLUTION; - const normalized = imageSize.toUpperCase(); - if (["1K", "2K", "4K"].includes(normalized)) return normalized; + const normalized = imageSize.trim().toLowerCase(); + if (["1k", "2k", "4k"].includes(normalized)) return normalized; return DEFAULT_RESOLUTION; } +function normalizeOutputFormat(outputFormat?: string): "png" | "jpg" { + if (!outputFormat) return DEFAULT_OUTPUT_FORMAT; + const normalized = outputFormat.trim().toLowerCase(); + if (normalized === "jpg" || normalized === "jpeg") return "jpg"; + return "png"; +} + function extractTaskId(payload: any): string | undefined { return ( payload?.taskId || @@ -107,9 +120,14 @@ function extractPromptAndImages(messages: Array<{ role: string; content: any }>) const imageUrls: string[] = []; const pushImageUrl = (url?: string) => { - if (!url || !url.startsWith("http")) return; - if (!imageUrls.includes(url)) { - imageUrls.push(url); + if (!url) return; + const normalized = String(url).trim(); + if (normalized.startsWith("data:")) { + throw new Error("RUXA 不支持 Base64 图片传输,请先上传图片后再编辑"); + } + if (!normalized.startsWith("http")) return; + if (!imageUrls.includes(normalized)) { + imageUrls.push(normalized); } }; @@ -325,7 +343,7 @@ export function createRuxaImage({ apiKey, baseURL }: RuxaImageConfig) { const { prompt, imageUrls } = extractPromptAndImages(options.messages); const aspectRatio = normalizeAspectRatio(options.imageParams?.aspectRatio); const resolution = normalizeResolution(options.imageParams?.imageSize); - const outputFormat = options.imageParams?.outputFormat || DEFAULT_OUTPUT_FORMAT; + const outputFormat = normalizeOutputFormat(options.imageParams?.outputFormat); const activeModelId = options.overrideModelId || modelId; const createEndpoint = joinApiPath(baseURL, "/api/v1/tasks/create");