From 161c04e23e1a2361211afc34c48b83862813b290 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 13 Mar 2026 17:07:39 +0800 Subject: [PATCH] fix: surface chat stream provider failures --- app/(chat)/api/chat/route.ts | 94 +++++++++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 988b37803..d59d702fd 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -1192,6 +1192,46 @@ function getDirectAgentToolErrorMessage( } } +function getChatStreamErrorMessage(error: unknown): string { + const rawMessage = + error instanceof Error ? String(error.message || "").trim() : String(error || "").trim(); + const normalized = rawMessage.toLowerCase(); + + if ( + normalized.includes("token quota is not enough") || + normalized.includes("pre_consume_token_quota_failed") || + normalized.includes("quota is not enough") + ) { + return "当前对话模型额度不足,请切换其他模型,或联系管理员补充该模型额度。"; + } + + if ( + normalized.includes("timeout") || + normalized.includes("timed out") || + normalized.includes("body timeout") + ) { + return "当前对话模型响应超时,请重试,或切换到其他可用模型。"; + } + + if (rawMessage) { + return `当前对话暂时失败:${rawMessage}`; + } + + return "当前对话暂时失败,请重试或切换其他模型。"; +} + +function normalizeAssistantPartsForPersistence(parts: Array> | undefined) { + const safeParts = Array.isArray(parts) ? parts.filter(Boolean) : []; + return safeParts.filter((part) => { + const partType = String(part?.type || "").trim(); + if (!partType) return false; + if (partType === "data-keepalive" || partType === "step-start" || partType === "step-finish") { + return false; + } + return true; + }); +} + function buildAgentToolImageMarkdown(params: { originalUrl: string; thumbnailUrl?: string | null; @@ -9187,11 +9227,19 @@ export async function POST(request: Request) { }, }); - result.consumeStream(); + result.consumeStream({ + onError: (error) => { + console.error("[Chat API] streamText consume error:", error); + }, + }); dataStream.merge( result.toUIMessageStream({ sendReasoning: true, + onError: (error) => { + console.error("[Chat API] streamText ui stream error:", error); + return getChatStreamErrorMessage(error); + }, }) ); } finally { @@ -9203,11 +9251,39 @@ export async function POST(request: Request) { onFinish: async ({ messages }) => { stopStreamKeepalive?.(); stopStreamKeepalive = null; - await saveMessages({ - messages: messages.map((currentMessage) => ({ + const messagesToPersist: Array<{ + id: string; + role: string; + parts: unknown; + createdAt: Date; + attachments: unknown; + chatId: string; + videoStorageStatus: null; + temporaryVideoUrl: null; + permanentVideoUrl: null; + archivedAt: null; + videoMetadata: null; + }> = []; + + for (const currentMessage of messages) { + const normalizedParts = + currentMessage.role === "assistant" + ? normalizeAssistantPartsForPersistence( + currentMessage.parts as Array> | undefined, + ) + : currentMessage.parts; + + if ( + currentMessage.role === "assistant" && + (!Array.isArray(normalizedParts) || normalizedParts.length === 0) + ) { + continue; + } + + messagesToPersist.push({ id: currentMessage.id, role: currentMessage.role, - parts: currentMessage.parts, + parts: normalizedParts, createdAt: new Date(), attachments: [], chatId: id, @@ -9216,8 +9292,14 @@ export async function POST(request: Request) { permanentVideoUrl: null, archivedAt: null, videoMetadata: null, - })), - }); + }); + } + + if (messagesToPersist.length > 0) { + await saveMessages({ + messages: messagesToPersist, + }); + } // 检测助手消息中是否包含视频,如果有则立即标记并归档到永久存储 for (const msg of messages) {