fix grsai gpt image edit inputs

This commit is contained in:
134 2026-06-12 10:29:01 +08:00
parent 04972684c3
commit 1aaf904fd3

View File

@ -283,6 +283,61 @@ interface GeminiResponse {
}>;
}
function joinOpenAICompatibleBaseUrl(baseURL: string, path: string) {
const normalized = String(baseURL || "").trim().replace(/\/+$/, "");
const withoutVersion = normalized.replace(/\/v1$/i, "");
return `${withoutVersion}${path}`;
}
async function loadImageAsBlob(imageUrl: string): Promise<{ blob: Blob; filename: string }> {
const normalized = String(imageUrl || "").trim();
if (!normalized) {
throw new Error("参考图地址为空");
}
if (normalized.startsWith("data:")) {
const match = normalized.match(/^data:([A-Za-z0-9.+/-]+);base64,(.+)$/);
if (!match) {
throw new Error("参考图 Base64 格式无效");
}
const mimeType = match[1] || "image/png";
const ext = mimeType.split("/")[1] || "png";
return {
blob: new Blob([Buffer.from(match[2], "base64")], { type: mimeType }),
filename: `reference.${ext}`,
};
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
try {
const response = await fetch(normalized, {
signal: controller.signal,
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get("content-type") || "image/png";
const extFromMime = contentType.split("/")[1]?.split(";")[0] || "png";
const extFromUrl = normalized
.split("?")[0]
.match(/\.([A-Za-z0-9]+)$/)?.[1];
const ext = extFromUrl || extFromMime;
return {
blob: new Blob([await response.arrayBuffer()], { type: contentType }),
filename: `reference.${ext}`,
};
} finally {
clearTimeout(timeoutId);
}
}
/**
* 从文本中提取图片数据
* 返回 { url, isBase64, mimeType, base64Data } 数组
@ -319,7 +374,35 @@ async function extractAndUploadImages(response: any): Promise<Array<{ thumbnail:
const images: Array<{ thumbnail: string; original: string }> = [];
const uploadPromises: Promise<void>[] = [];
// 1. 尝试 OpenAI 格式 (choices[0].message.content)
// 1. 尝试 OpenAI Images API 格式 (data[].b64_json / data[].url)
if (Array.isArray(response.data)) {
logger.debug("[Gemini Image] Parsing OpenAI Images format, data count:", response.data.length);
for (const item of response.data) {
if (item?.b64_json && typeof item.b64_json === "string") {
const base64Data = item.b64_json.startsWith("data:")
? item.b64_json.split(",", 2)[1] || item.b64_json
: item.b64_json;
const uploadPromise = uploadBase64ToS3(base64Data, "image/png")
.then((urls) => {
images.push(urls);
});
uploadPromises.push(uploadPromise);
} else if (item?.url && typeof item.url === "string") {
logger.debug("[Gemini Image] Found OpenAI Images URL, will download and upload to S3:", item.url.substring(0, 100) + "...");
const uploadPromise = downloadAndUploadToS3(item.url)
.then((urls) => {
images.push(urls);
})
.catch((error) => {
logger.error("[Gemini Image] Failed to download/upload OpenAI Images URL after retries:", error);
throw new Error(`无法下载并上传图片到国内中转: ${error instanceof Error ? error.message : "未知错误"}`);
});
uploadPromises.push(uploadPromise);
}
}
}
// 2. 尝试 OpenAI Chat 格式 (choices[0].message.content)
if (response.choices && response.choices.length > 0) {
const content = response.choices[0]?.message?.content;
if (content && typeof content === "string") {
@ -351,7 +434,7 @@ async function extractAndUploadImages(response: any): Promise<Array<{ thumbnail:
}
}
// 2. 尝试 Nano Banana API 格式 (results[].url)
// 3. 尝试 Nano Banana API 格式 (results[].url)
if (response.results && Array.isArray(response.results)) {
logger.debug("[Gemini Image] Parsing Nano Banana format, results count:", response.results.length);
for (const result of response.results) {
@ -374,7 +457,7 @@ async function extractAndUploadImages(response: any): Promise<Array<{ thumbnail:
}
}
// 3. 尝试 Gemini 格式 (candidates[0].content.parts)
// 4. 尝试 Gemini 格式 (candidates[0].content.parts)
if (response.candidates && response.candidates.length > 0) {
const candidate = response.candidates[0];
if (candidate.content && candidate.content.parts) {
@ -462,6 +545,107 @@ function normalizeGrsaiPayload(payload: any): any {
return payload;
}
async function sendGrsaiGptImageRequest(params: {
baseURL: string;
apiKey: string;
modelId: string;
prompt: string;
size: string;
imageUrls: string[];
timeout: number;
apiLogPrefix: string;
}): Promise<any> {
const { baseURL, apiKey, modelId, prompt, size, imageUrls, timeout, apiLogPrefix } = params;
const hasReferenceImage = imageUrls.length > 0;
const endpoint = joinOpenAICompatibleBaseUrl(
baseURL,
hasReferenceImage ? "/v1/images/edits" : "/v1/images/generations"
);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
let response: Response;
if (hasReferenceImage) {
const formData = new FormData();
formData.append("model", modelId);
formData.append("prompt", prompt);
formData.append("size", size);
const { blob, filename } = await loadImageAsBlob(imageUrls[0]);
formData.append("image", blob, filename);
logger.debug(
`${apiLogPrefix} Sending OpenAI-compatible edit request: endpoint=${endpoint}, size=${size}, referenceImageCount=${imageUrls.length}`
);
response = await fetch(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
},
body: formData,
signal: controller.signal,
});
} else {
const requestBody = {
model: modelId,
prompt,
size,
};
logger.debug(
`${apiLogPrefix} Sending OpenAI-compatible generation request: endpoint=${endpoint}, size=${size}`
);
response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(requestBody),
signal: controller.signal,
});
}
clearTimeout(timeoutId);
const responseText = await response.text();
let data: any = null;
try {
data = responseText ? JSON.parse(responseText) : {};
} catch {
data = { raw: responseText };
}
if (!response.ok) {
logger.error(
`${apiLogPrefix} OpenAI-compatible request failed:`,
response.status,
responseText.substring(0, 1000)
);
const errorMessage =
data?.error?.message ||
data?.message ||
data?.error ||
responseText ||
`${response.status} ${response.statusText}`;
throw new Error(`图片生成失败: ${errorMessage}`);
}
return data;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === "AbortError") {
const timeoutSeconds = Math.round(timeout / 1000);
logger.error(`${apiLogPrefix} Request timeout after ${timeoutSeconds} seconds`);
throw new Error(`Grsai GPT Image 请求超时(${timeoutSeconds}秒),将自动切换备用模型`);
}
throw error;
}
}
function getGrsaiTaskId(payload: any): string | null {
const normalized = normalizeGrsaiPayload(payload);
const candidates = [
@ -818,6 +1002,19 @@ async function sendGeminiRequest(
logger.debug(
`${apiLogPrefix} Resolved size: aspectRatio=${resolvedAspectRatio}, imageSize=${imageParams?.imageSize || "2K"} => ${resolvedSize}`
);
const timeout = imageParams?.imageSize === "4K" ? 300000 : 120000;
logger.debug(`${apiLogPrefix} Timeout set to ${timeout / 1000} seconds for ${imageParams?.imageSize || "default"}`);
return sendGrsaiGptImageRequest({
baseURL,
apiKey,
modelId,
prompt,
size: resolvedSize,
imageUrls: processedUrls,
timeout,
apiLogPrefix,
});
} else {
// 添加 Nano Banana 特定参数
if (processedUrls.length > 0) {