From ce283d287f9330cfca1efbc7a10d2266b063de7f Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 20 May 2026 20:59:26 +0800 Subject: [PATCH] fix: classify chat send failures --- app/(chat)/api/chat/route.ts | 174 ++++++++++++++++++++++++++++++++++- lib/errors.ts | 19 +++- lib/request-error.ts | 12 +++ 3 files changed, 201 insertions(+), 4 deletions(-) diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index b1158ba78..2e868925d 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -1200,6 +1200,107 @@ function getChatStreamErrorMessage(error: unknown): string { return "当前对话暂时失败,请重试或切换其他模型。"; } +function resolveChatApiFailure(error: unknown) { + const detailMessage = getRequestErrorMessage(error); + const normalized = String(detailMessage || "").toLowerCase(); + + if ( + normalized.includes("payload too large") || + normalized.includes("request entity too large") || + normalized.includes("body exceeded") || + normalized.includes("content length") || + normalized.includes("413") + ) { + return { + code: "payload_too_large:chat", + message: "本次请求内容过大,请减少图片或文字内容后重试。", + status: 413, + category: "payload_too_large", + detailMessage, + }; + } + + if ( + error instanceof OperationTimeoutError || + normalized.includes("timeout") || + normalized.includes("timed out") || + normalized.includes("body timeout") || + normalized.includes("aborterror") || + normalized.includes("aborted") + ) { + return { + code: "timeout:chat", + message: "当前模型响应超时,请重试,或切换到其他可用模型。", + status: 504, + category: "timeout", + detailMessage, + }; + } + + if ( + normalized.includes("fetch failed") || + normalized.includes("econnreset") || + normalized.includes("econnrefused") || + normalized.includes("enotfound") || + normalized.includes("etimedout") || + normalized.includes("und_err") || + normalized.includes("socket hang up") + ) { + return { + code: "server_error:chat", + message: "模型服务连接失败,请稍后重试,或切换其他可用模型。", + status: 502, + category: "upstream_connection", + detailMessage, + }; + } + + if ( + normalized.includes("database") || + normalized.includes("postgres") || + normalized.includes("drizzle") + ) { + return { + code: "server_error:chat", + message: "服务数据库暂时繁忙,请稍后重试。", + status: 503, + category: "database", + detailMessage, + }; + } + + return { + code: "server_error:chat", + message: "服务临时繁忙,请稍后重试;如果连续出现,请联系管理员。", + status: 500, + category: "unknown", + detailMessage, + }; +} + +function createChatApiFailureResponse(error: unknown, vercelId: string | null) { + const failure = resolveChatApiFailure(error); + const traceId = generateUUID(); + + console.error("[Chat API] Unhandled request failure:", { + traceId, + vercelId, + category: failure.category, + status: failure.status, + detailMessage: failure.detailMessage, + error, + }); + + return NextResponse.json( + { + code: failure.code, + message: failure.message, + cause: `trace:${traceId}`, + }, + { status: failure.status } + ); +} + function normalizeAssistantPartsForPersistence(parts: Array> | undefined) { const safeParts = Array.isArray(parts) ? parts.filter(Boolean) : []; return safeParts.filter((part) => { @@ -9903,6 +10004,76 @@ export async function POST(request: Request) { // 获取启用的工具列表 const enabledTools = await getEnabledTools(); + if (String(finalModelId || "").trim() === "auto") { + const hasImageInputForAutoFallback = + message.parts.some((part: any) => + part?.type === "file" || part?.type === "image" || part?.type === "image_url" + ) || smartAutoHistoryImages.length > 0; + const autoFallbackIntent = + smartAutoExecution?.shouldApplyVideoPipeline || composerMode === "video" + ? "video_gen" + : smartAutoExecution?.shouldApplyImagePipeline || + composerMode === "image" || + composerMode === "detail" + ? hasImageInputForAutoFallback + ? "image_edit" + : "image_gen" + : hasImageInputForAutoFallback + ? "vision" + : "chat"; + + try { + const { routeToModel } = await import("@/lib/ai/model-router"); + const fallbackRoute = await routeToModel({ + intent: autoFallbackIntent, + hasImage: hasImageInputForAutoFallback, + }); + finalModelId = fallbackRoute.id; + console.warn("[Chat API] Auto model was unresolved, routed to fallback model:", { + intent: autoFallbackIntent, + modelId: fallbackRoute.id, + displayName: fallbackRoute.displayName, + reason: fallbackRoute.reason, + }); + } catch (autoModelRouteError) { + console.error("[Chat API] Failed to route unresolved auto model:", autoModelRouteError); + const enabledModels = await getEnabledAiModelsWithProvider(false); + const nonAgentModels = enabledModels.filter(({ model }) => !isAiModelAgentAuto(model)); + const candidates = nonAgentModels.length > 0 ? nonAgentModels : enabledModels; + const fallbackModel = + candidates.find(({ model, provider }) => { + if (!model.isEnabled || model.isSubModel) return false; + const providerType = String(provider?.type || ""); + if (autoFallbackIntent === "video_gen") { + return isVideoProviderType(providerType, model); + } + if (autoFallbackIntent === "image_edit" || autoFallbackIntent === "image_gen") { + return isImageGenerationModelType(providerType, model); + } + return !isVideoProviderType(providerType, model) && !model.supportsImageGen; + }) || + candidates.find( + ({ model, provider }) => + model.isEnabled && + !model.isSubModel && + !isVideoProviderType(String(provider?.type || ""), model) + ) || + candidates.find(({ model }) => model.isEnabled && !model.isSubModel); + + if (!fallbackModel) { + return new ChatSDKError("bad_request:api", "No available models for auto mode").toResponse(); + } + + finalModelId = fallbackModel.model.id; + console.warn("[Chat API] Auto model fallback used first available model:", { + intent: autoFallbackIntent, + modelId: fallbackModel.model.id, + displayName: fallbackModel.model.displayName, + providerType: fallbackModel.provider?.type, + }); + } + } + // 检查是否是 UUID 格式(数据库中的模型 ID) const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( @@ -13542,8 +13713,7 @@ export async function POST(request: Request) { return new ChatSDKError("bad_request:activate_gateway").toResponse(); } - console.error("Unhandled error in chat API:", error, { vercelId }); - return new ChatSDKError("offline:chat").toResponse(); + return createChatApiFailureResponse(error, vercelId); } finally { if (requestDedupKey && acquiredInFlightLock) { inFlightChatRequests.delete(requestDedupKey); diff --git a/lib/errors.ts b/lib/errors.ts index d7b92263c..c51c5780e 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -4,7 +4,10 @@ export type ErrorType = | "forbidden" | "not_found" | "rate_limit" - | "offline"; + | "offline" + | "payload_too_large" + | "server_error" + | "timeout"; export type Surface = | "chat" @@ -105,7 +108,13 @@ export function getMessageByErrorCode(errorCode: ErrorCode): string { case "unauthorized:chat": return "You need to sign in to view this chat. Please sign in and try again."; case "offline:chat": - return "We're having trouble sending your message. Please check your internet connection and try again."; + return "网络连接已断开,请检查网络后重试。"; + case "payload_too_large:chat": + return "本次请求内容过大,请减少图片或文字内容后重试。"; + case "server_error:chat": + return "服务临时繁忙,请稍后重试;如果连续出现,请联系管理员。"; + case "timeout:chat": + return "当前模型响应超时,请重试,或切换到其他可用模型。"; case "not_found:document": return "The requested document was not found. Please check the document ID and try again."; @@ -145,6 +154,12 @@ function getStatusCodeByType(type: ErrorType) { return 429; case "offline": return 503; + case "payload_too_large": + return 413; + case "server_error": + return 500; + case "timeout": + return 504; default: return 500; } diff --git a/lib/request-error.ts b/lib/request-error.ts index f343b7e25..906f57f60 100644 --- a/lib/request-error.ts +++ b/lib/request-error.ts @@ -9,6 +9,14 @@ const RATE_LIMIT_PATTERNS = [ /达到速率限制/, /限流/, ]; +const NETWORK_TRANSPORT_PATTERNS = [ + /\bfailed to fetch\b/i, + /\bnetworkerror\b/i, + /\bload failed\b/i, + /network request failed/i, + /having trouble sending your message/i, + /check your internet connection/i, +]; const JSON_EMBEDDED_MESSAGE_PATTERN = /^[["{]/; function isRecord(value: unknown): value is Record { @@ -186,5 +194,9 @@ export function getUserFacingRequestErrorMessage( return getFriendlyRateLimitMessage(options?.rateLimitMessage); } + if (NETWORK_TRANSPORT_PATTERNS.some((pattern) => pattern.test(message))) { + return "请求发送失败,请检查网络或稍后重试。"; + } + return message; }