diff --git a/components/elements/response.tsx b/components/elements/response.tsx index 932d24c9e..83126a680 100644 --- a/components/elements/response.tsx +++ b/components/elements/response.tsx @@ -18,6 +18,7 @@ import { ResponsePdfDownloadButton } from "@/components/elements/response-pdf-do import { buildImageProxyUrl } from "@/lib/image-delivery"; import { splitMarkdownImages } from "@/lib/canvas/markdown-image-segments"; import { stableStreamingMarkdown } from "@/lib/canvas/markdown-image-streaming"; +import { getGatewayErrorPageMessage } from "@/lib/request-error"; import { Collapsible, CollapsibleContent, @@ -1028,7 +1029,10 @@ export const Response = ({ const rawText = typeof props.children === "string" - ? normalizeMarkdownContent(stableStreamingMarkdown(props.children, isMarkdownStreaming)) + ? normalizeMarkdownContent(stableStreamingMarkdown( + getGatewayErrorPageMessage(props.children) || props.children, + isMarkdownStreaming + )) : null; const shouldShowPdfDownload = Boolean(enablePdfDownload && rawText && hasDownloadableReportContent(rawText)); diff --git a/components/message.tsx b/components/message.tsx index 790b935c4..cabc216ba 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -7,6 +7,7 @@ import type { Vote } from "@/lib/db/schema"; import type { ChatMessage } from "@/lib/types"; import { cn, sanitizeText } from "@/lib/utils"; import { extractImageUrlsFromText } from "@/lib/image-url"; +import { getGatewayErrorPageMessage } from "@/lib/request-error"; import { useDataStream } from "./data-stream-provider"; import { DocumentToolResult } from "./document"; import { DocumentPreview } from "./document-preview"; @@ -521,7 +522,7 @@ const PurePreviewMessage = ({ const displayModelName = extractedModelName || inferredFallbackModelName; const filteredText = message.role === "assistant" - ? appendImageDoneSummary(contentWithoutBadge) + ? getGatewayErrorPageMessage(contentWithoutBadge) || appendImageDoneSummary(contentWithoutBadge) : contentWithoutBadge; const useAssistantTypewriter = message.role === "assistant" && diff --git a/lib/ai/providers/vip98k.ts b/lib/ai/providers/vip98k.ts index 386ad542b..b123fb6ab 100644 --- a/lib/ai/providers/vip98k.ts +++ b/lib/ai/providers/vip98k.ts @@ -48,6 +48,7 @@ import { VIP98K_REFERENCE_STAGING_KEY_PREFIX, } from "@/lib/ai/vip98k-reference-staging"; import { createLogger } from "@/lib/observability/logger"; +import { getUserFacingImageGenerationErrorMessage } from "@/lib/request-error"; import { createDeferredUploadedImage } from "@/lib/storage/image-archive"; import { IMAGE_UPLOAD_MAX_BYTES, @@ -1188,9 +1189,9 @@ async function submitTask(params: { continue; } if ((response.status === 409 && /idempotency\s+key.*(?:still\s+in\s+progress|processing)/i.test(errorMessage)) - || [502, 504].includes(response.status) + || [502, 504, 524].includes(response.status) || /pending\s+safe\s+reconciliation|request\s+outcome\s+(?:is\s+)?unknown|final\s+outcome\s+could\s+not\s+be\s+confirmed/i.test(errorMessage)) - throw new ProviderTaskUncertainError(`供应商提交结果未确认,可能仍在处理,请勿重复生成:${errorMessage} (HTTP ${response.status})`); + throw new ProviderTaskUncertainError(`供应商提交结果未确认,可能仍在处理,请勿重复生成:${getUserFacingImageGenerationErrorMessage({ message: errorMessage, status: response.status })} (HTTP ${response.status})`); throw new Error(`${errorMessage} (HTTP ${response.status})`); } diff --git a/lib/ai/runtime/model-health.ts b/lib/ai/runtime/model-health.ts index 22ab80038..b1403b5a0 100644 --- a/lib/ai/runtime/model-health.ts +++ b/lib/ai/runtime/model-health.ts @@ -12,7 +12,7 @@ import { isMediaInputFailure, isNonProviderRequestStatus, } from "@/lib/ai/runtime/request-failure-classification"; -import { isProviderBalanceError, isRateLimitError } from "@/lib/request-error"; +import { isGatewayResponseTimeout, isProviderBalanceError, isRateLimitError } from "@/lib/request-error"; export type AiModelHealthEventType = | "success" @@ -343,6 +343,7 @@ export function classifyAiModelHealthFailure(error: unknown): { } export function shouldFailoverAiModelError(error: unknown) { + if (isGatewayResponseTimeout(error)) return false; const classification = classifyAiModelHealthFailure(error); const normalized = getErrorMessage(error).toLowerCase(); const imageProviderTaskFailed = diff --git a/lib/request-error.ts b/lib/request-error.ts index b0dc5d3e6..3a060050f 100644 --- a/lib/request-error.ts +++ b/lib/request-error.ts @@ -175,6 +175,28 @@ function getRequestErrorText(error: unknown) { return candidates.join(" ").slice(0, 4000); } +export function getGatewayErrorPageMessage(content: string): string | null { + if (!/^\s*(?: pattern.test(message)); @@ -203,6 +225,8 @@ export function getUserFacingImageGenerationErrorMessage( error: unknown, fallbackMessage = "图片生成失败,请稍后重试", ) { + const gatewayMessage = getPublicGatewayErrorMessage(error); + if (gatewayMessage) return gatewayMessage; if (isProviderBalanceError(error)) { return "图片服务商线路余额或额度不足,请切换模型或联系管理员;与您的积分余额无关。"; } @@ -227,6 +251,8 @@ export function getUserFacingRequestErrorMessage( rateLimitMessage?: string; } ) { + const gatewayMessage = getPublicGatewayErrorMessage(error); + if (gatewayMessage) return gatewayMessage; if (isProviderBalanceError(error)) { return "模型服务商线路余额或额度不足,请切换模型或联系管理员。"; } diff --git a/public/particle-scatter-effect.html b/public/particle-scatter-effect.html index e7579bfba..ce94ef695 100644 --- a/public/particle-scatter-effect.html +++ b/public/particle-scatter-effect.html @@ -4,7 +4,6 @@ Neural Particle - Scatter Effect -