diff --git a/lib/ai/providers/gemini-image.ts b/lib/ai/providers/gemini-image.ts index b3c5c5fb1..b507a3940 100644 --- a/lib/ai/providers/gemini-image.ts +++ b/lib/ai/providers/gemini-image.ts @@ -716,12 +716,11 @@ async function sendGrsaiGptImageRequest(params: { response.status, responseText.substring(0, 1000) ); + const providerError = getGrsaiErrorMessage(data); const errorMessage = - data?.error?.message || - data?.message || - data?.error || - responseText || - `${response.status} ${response.statusText}`; + providerError !== "未知错误" + ? providerError + : responseText || `${response.status} ${response.statusText}`; throw new Error(`图片生成失败: ${errorMessage}`); } @@ -754,6 +753,13 @@ async function sendGrsaiGptImageRequest(params: { timeoutLabel: "Grsai GPT Image", }); } + + const providerError = getGrsaiErrorMessage(data); + throw new Error( + `图片生成失败: 未返回任务 ID 或可用图片${ + providerError !== "未知错误" ? `: ${providerError}` : "" + }` + ); } return data; @@ -857,18 +863,46 @@ function getGrsaiProgress(payload: any): number | null { function getGrsaiErrorMessage(payload: any): string { const normalized = normalizeGrsaiPayload(payload); + const normalizePart = (part: unknown) => { + if (typeof part === "string" || typeof part === "number") { + return String(part).trim(); + } + if (!isRecord(part)) return ""; + return String( + part.message || + part.msg || + part.detail || + part.reason || + part.code || + part.type || + "" + ).trim(); + }; const messages = [ normalized?.failure_reason, + normalized?.error?.message, + normalized?.error?.msg, + normalized?.error?.code, normalized?.error, normalized?.message, + normalized?.msg, + normalized?.detail, + normalized?.reason, + normalized?.code, + payload?.error?.message, + payload?.error?.msg, + payload?.error?.code, payload?.msg, payload?.message, payload?.error, + payload?.detail, + payload?.reason, + payload?.code, ] - .map((part) => String(part || "").trim()) + .map(normalizePart) .filter(Boolean); - return messages.join(" / ") || "未知错误"; + return Array.from(new Set(messages)).join(" / ") || "未知错误"; } function hasGrsaiImages(payload: any): boolean { @@ -899,18 +933,36 @@ async function fetchWithTimeout( async function readJsonResponse(response: Response, label: string): Promise { const text = await response.text(); + const payload = parseGrsaiJsonOrStreamResponse(text); if (!response.ok) { logger.error(`${label} failed:`, response.status, text.substring(0, 500)); - throw new Error(`${label} 接口错误:${response.status} ${response.statusText}`); + const providerError = getGrsaiErrorMessage(payload); + const detail = + providerError !== "未知错误" + ? providerError + : String(payload?.raw || text || "").trim().slice(0, 300); + throw new Error( + `${label} 接口错误:${response.status} ${response.statusText}${ + detail ? `: ${detail}` : "" + }` + ); } - try { - return JSON.parse(text); - } catch (error) { + if (isRecord(payload) && !("raw" in payload)) { + return payload; + } + + if (!isRecord(payload)) { + return payload; + } + + if ("raw" in payload) { logger.error(`${label} returned non-JSON response:`, text.substring(0, 500)); throw new Error(`${label} 返回格式异常,未能解析任务结果`); } + + return payload; } async function pollGrsaiNanoBananaResult(params: { @@ -1047,11 +1099,16 @@ async function submitAndPollGrsaiNanoBanana(params: { const taskId = getGrsaiTaskId(submitPayload); if (!taskId) { + const providerError = getGrsaiErrorMessage(submitPayload); logger.error( `${params.apiLogPrefix} Async submit response has no task id:`, JSON.stringify(submitPayload).substring(0, 500) ); - throw new Error("GRSAI Nano Banana 异步任务提交失败:未返回任务 ID"); + throw new Error( + `GRSAI Nano Banana 异步任务提交失败:未返回任务 ID${ + providerError !== "未知错误" ? `: ${providerError}` : "" + }` + ); } logger.debug(`${params.apiLogPrefix} Async task submitted:`, { taskId }); diff --git a/lib/ai/runtime/model-health.ts b/lib/ai/runtime/model-health.ts index 765cf910c..8ab443831 100644 --- a/lib/ai/runtime/model-health.ts +++ b/lib/ai/runtime/model-health.ts @@ -151,7 +151,10 @@ export function classifyAiModelHealthFailure(error: unknown): { if ( normalized.includes("insufficient balance") || + normalized.includes("insufficient credit") || normalized.includes("credits insufficient") || + normalized.includes("credit insufficient") || + normalized.includes("not enough credit") || normalized.includes("quota") || normalized.includes("余额") || normalized.includes("额度") || @@ -209,10 +212,38 @@ export function classifyAiModelHealthFailure(error: unknown): { }; } + if ( + normalized.includes("未返回任务 id") || + normalized.includes("未返回任务id") || + normalized.includes("no task id") || + normalized.includes("missing task id") || + normalized.includes("provider error") || + normalized.includes("provider_error") || + normalized.includes("provider request failed") || + normalized.includes("provider task failed") || + normalized.includes("upstream error") || + normalized.includes("upstream_error") || + normalized.includes("upstream request failed") || + normalized.includes("上游服务错误") || + normalized.includes("上游接口错误") + ) { + return { + eventType: "unavailable", + severity: "critical", + counted: true, + reason: "provider_execution_failed", + }; + } + if ( normalized.includes("download") || normalized.includes("no image url") || normalized.includes("no image") || + normalized.includes("empty result") || + normalized.includes("empty response") || + normalized.includes("空结果") || + normalized.includes("返回结果为空") || + normalized.includes("响应为空") || normalized.includes("未返回可用图片") || normalized.includes("未包含可用图片") || normalized.includes("未解析到图片") || @@ -245,14 +276,21 @@ export function shouldFailoverAiModelError(error: unknown) { normalized.includes("snapgen 任务已完成但未返回图片地址") || normalized.includes("snapgen 未返回可用图片"); - if (imageProviderTaskFailed) { - return true; - } + const providerExecutionFailed = + classification.reason === "provider_execution_failed" || + normalized.includes("未返回任务 id") || + normalized.includes("未返回任务id") || + normalized.includes("no task id") || + normalized.includes("missing task id"); if (classification.reason === "user_cancelled" || classification.reason === "non_provider_failure") { return false; } + if (imageProviderTaskFailed || providerExecutionFailed) { + return true; + } + if ( classification.eventType === "timeout" || classification.reason === "transport_failure_no_provider_response" @@ -263,6 +301,11 @@ export function shouldFailoverAiModelError(error: unknown) { if (classification.eventType === "download_error") { return ( normalized.includes("no image") || + normalized.includes("empty result") || + normalized.includes("empty response") || + normalized.includes("空结果") || + normalized.includes("返回结果为空") || + normalized.includes("响应为空") || normalized.includes("未返回可用图片") || normalized.includes("未包含可用图片") || normalized.includes("未解析到图片") ||