fix grsai gpt image edit inputs
This commit is contained in:
parent
04972684c3
commit
1aaf904fd3
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user