fix: normalize rate limit errors across chat and PSD flows
This commit is contained in:
parent
03f07564e0
commit
411bb4a5eb
@ -139,6 +139,11 @@ import type {
|
||||
ResolvedTargetAssets,
|
||||
} from "@/lib/ai/context-memory/types";
|
||||
import { ChatSDKError } from "@/lib/errors";
|
||||
import {
|
||||
getFriendlyRateLimitMessage,
|
||||
getRequestErrorMessage,
|
||||
isRateLimitError,
|
||||
} from "@/lib/request-error";
|
||||
import { extractImageUrlsFromText } from "@/lib/image-url";
|
||||
import {
|
||||
resolveClarificationBypass,
|
||||
@ -12637,6 +12642,28 @@ export async function POST(request: Request) {
|
||||
return error.toResponse();
|
||||
}
|
||||
|
||||
if (isRateLimitError(error)) {
|
||||
const detailMessage = getRequestErrorMessage(error);
|
||||
console.warn("[Chat API] Upstream rate limited request:", {
|
||||
vercelId,
|
||||
error: detailMessage,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: "rate_limit:chat",
|
||||
message: getFriendlyRateLimitMessage(),
|
||||
cause: detailMessage || "upstream_rate_limited",
|
||||
retryAfter: 10,
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
"Retry-After": "10",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Check for Vercel AI Gateway credit card error
|
||||
if (
|
||||
error instanceof Error &&
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { ExtendServiceError, executeExtendJob } from "@/lib/ai/extend/service";
|
||||
import {
|
||||
executeExtendJob,
|
||||
ExtendServiceError,
|
||||
} from "@/lib/ai/extend/service";
|
||||
getFriendlyRateLimitMessage,
|
||||
getUserFacingRequestErrorMessage,
|
||||
isRateLimitError,
|
||||
} from "@/lib/request-error";
|
||||
|
||||
export const maxDuration = 180;
|
||||
|
||||
@ -89,9 +91,25 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof ExtendServiceError) {
|
||||
if (error.status === 429 || isRateLimitError(error)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getFriendlyRateLimitMessage("当前操作过于频繁,请稍后再试"),
|
||||
detail: error.detail || error.message,
|
||||
retryAfter: 10,
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
"Retry-After": "10",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error.message,
|
||||
error: getUserFacingRequestErrorMessage(error, "图像拓展失败"),
|
||||
detail: error.detail || error.message,
|
||||
},
|
||||
{ status: error.status },
|
||||
@ -99,9 +117,24 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
console.error("[Extend API] 错误:", error);
|
||||
if (isRateLimitError(error)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getFriendlyRateLimitMessage("当前操作过于频繁,请稍后再试"),
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
retryAfter: 10,
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
"Retry-After": "10",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "图像拓展失败",
|
||||
error: getUserFacingRequestErrorMessage(error, "图像拓展失败"),
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status: 500 },
|
||||
|
||||
@ -1,22 +1,27 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import {
|
||||
getEditableAgentPsdTemplateLayers,
|
||||
getAgentPsdTemplateRuntimeIssue,
|
||||
isAgentPsdImageLayerSlot,
|
||||
isAgentPsdTemplateRuntimeReady,
|
||||
isAgentPsdTemplateReady,
|
||||
isAgentPsdTextLayerSlot,
|
||||
type AgentPsdTemplateRenderResponse,
|
||||
} from "@/lib/agents/psd-template-config";
|
||||
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
||||
import { normalizeAgentPsdImagePlacement } from "@/lib/agents/psd-image-placement";
|
||||
import { describeRemoteAssetLocation } from "@/lib/agents/psd-remote-asset";
|
||||
import { repairAgentPsdTemplateFontBindings } from "@/lib/agents/psd-template-repair";
|
||||
import {
|
||||
type AgentPsdTemplateRenderResponse,
|
||||
getAgentPsdTemplateRuntimeIssue,
|
||||
getEditableAgentPsdTemplateLayers,
|
||||
isAgentPsdImageLayerSlot,
|
||||
isAgentPsdTemplateReady,
|
||||
isAgentPsdTemplateRuntimeReady,
|
||||
isAgentPsdTextLayerSlot,
|
||||
} from "@/lib/agents/psd-template-config";
|
||||
import { renderAgentPsdTemplate } from "@/lib/agents/psd-template-renderer";
|
||||
import { normalizeHexColor } from "@/lib/agents/psd-text-layout";
|
||||
import { repairAgentPsdTemplateFontBindings } from "@/lib/agents/psd-template-repair";
|
||||
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
||||
import { normalizeHexColor } from "@/lib/agents/psd-text-layout";
|
||||
import {
|
||||
getFriendlyRateLimitMessage,
|
||||
getUserFacingRequestErrorMessage,
|
||||
isRateLimitError,
|
||||
} from "@/lib/request-error";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
@ -163,10 +168,26 @@ export async function POST(request: NextRequest) {
|
||||
assignedImageCount: Object.keys(body.imageAssignments || {}).length,
|
||||
error,
|
||||
});
|
||||
|
||||
if (isRateLimitError(error)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: getFriendlyRateLimitMessage("当前操作过于频繁,请稍后再试"),
|
||||
} satisfies AgentPsdTemplateRenderResponse,
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
"Retry-After": "10",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const message =
|
||||
error instanceof Error && String(error.message || "").trim()
|
||||
? error.message
|
||||
: "PSD 模板渲染失败";
|
||||
getUserFacingRequestErrorMessage(error, "PSD 模板渲染失败", {
|
||||
rateLimitMessage: getFriendlyRateLimitMessage("当前操作过于频繁,请稍后再试"),
|
||||
});
|
||||
return jsonError(502, message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,6 +81,7 @@ import {
|
||||
resolveTextLayout,
|
||||
} from "@/lib/agents/psd-text-layout";
|
||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||
import { getFriendlyRateLimitMessage, getUserFacingRequestErrorMessage } from "@/lib/request-error";
|
||||
|
||||
function buildTemplateListRequestUrl(params?: {
|
||||
brandName?: string | null;
|
||||
@ -182,6 +183,55 @@ const templateDetailFetcher = async (
|
||||
}
|
||||
};
|
||||
|
||||
async function parseActionJsonResponse<T>(
|
||||
response: Response,
|
||||
fallbackMessage: string,
|
||||
rateLimitMessage?: string
|
||||
): Promise<T> {
|
||||
const payload = await response
|
||||
.clone()
|
||||
.json()
|
||||
.catch(() => null);
|
||||
|
||||
if (!response.ok) {
|
||||
let responseText = "";
|
||||
if (payload == null) {
|
||||
responseText = await response
|
||||
.text()
|
||||
.then((value) => value.trim())
|
||||
.catch(() => "");
|
||||
}
|
||||
|
||||
const requestError = new Error(
|
||||
getUserFacingRequestErrorMessage(
|
||||
{
|
||||
status: response.status,
|
||||
data: payload,
|
||||
message: responseText,
|
||||
},
|
||||
fallbackMessage,
|
||||
{
|
||||
rateLimitMessage,
|
||||
}
|
||||
)
|
||||
) as Error & {
|
||||
status?: number;
|
||||
data?: unknown;
|
||||
};
|
||||
requestError.status = response.status;
|
||||
requestError.data =
|
||||
payload ??
|
||||
(responseText
|
||||
? {
|
||||
message: responseText,
|
||||
}
|
||||
: null);
|
||||
throw requestError;
|
||||
}
|
||||
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
const PREVIEW_VIEWPORT_PADDING = 56;
|
||||
const PREVIEW_STAGE_MAX_SCALE = 0.82;
|
||||
const PREVIEW_STAGE_TARGET_WIDTH = 760;
|
||||
@ -1946,15 +1996,20 @@ export function AgentPsdTemplateEditorDialog({
|
||||
const formData = new FormData();
|
||||
formData.set("file", file);
|
||||
const response = await fetchUploadWithRetry(formData);
|
||||
const payload = await response.json();
|
||||
const payload = await parseActionJsonResponse<{ url?: string }>(
|
||||
response,
|
||||
"素材上传失败",
|
||||
getFriendlyRateLimitMessage("当前上传过于频繁,请稍后再试")
|
||||
);
|
||||
|
||||
if (!response.ok || !payload.url) {
|
||||
throw new Error(payload.error || "图片上传失败");
|
||||
if (!payload?.url) {
|
||||
throw new Error("图片上传失败");
|
||||
}
|
||||
const uploadedImageUrl = payload.url;
|
||||
|
||||
setImageAssignments((current) => ({
|
||||
...current,
|
||||
[slotId]: payload.url,
|
||||
[slotId]: uploadedImageUrl,
|
||||
}));
|
||||
setImagePlacements((current) => ({
|
||||
...current,
|
||||
@ -1967,7 +2022,11 @@ export function AgentPsdTemplateEditorDialog({
|
||||
});
|
||||
toast.success("替换图片已上传");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "素材上传失败");
|
||||
toast.error(
|
||||
getUserFacingRequestErrorMessage(error, "素材上传失败", {
|
||||
rateLimitMessage: getFriendlyRateLimitMessage("当前上传过于频繁,请稍后再试"),
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setUploadingSlotId(null);
|
||||
event.target.value = "";
|
||||
@ -1993,13 +2052,14 @@ export function AgentPsdTemplateEditorDialog({
|
||||
prompt: "PSD 套版图片拓展处理",
|
||||
}),
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
String(payload?.error || payload?.detail || "").trim() ||
|
||||
"图片拓展失败"
|
||||
);
|
||||
}
|
||||
const payload = await parseActionJsonResponse<{
|
||||
original_url?: string;
|
||||
thumbnail_url?: string;
|
||||
}>(
|
||||
response,
|
||||
"图片拓展失败",
|
||||
getFriendlyRateLimitMessage("当前图片拓展请求过于频繁,请稍后再试")
|
||||
);
|
||||
|
||||
const nextImageUrl = String(
|
||||
payload?.original_url || payload?.thumbnail_url || ""
|
||||
@ -2028,7 +2088,11 @@ export function AgentPsdTemplateEditorDialog({
|
||||
: "图片拓展完成,已替换当前槽位"
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "图片拓展失败");
|
||||
toast.error(
|
||||
getUserFacingRequestErrorMessage(error, "图片拓展失败", {
|
||||
rateLimitMessage: getFriendlyRateLimitMessage("当前图片拓展请求过于频繁,请稍后再试"),
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setExtendingSlotId(null);
|
||||
}
|
||||
@ -2154,9 +2218,13 @@ export function AgentPsdTemplateEditorDialog({
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = (await response.json()) as AgentPsdTemplateRenderResponse;
|
||||
if (!response.ok || !payload.success || !payload.result?.url) {
|
||||
throw new Error(payload.error || "PSD 模板渲染失败");
|
||||
const payload = await parseActionJsonResponse<AgentPsdTemplateRenderResponse>(
|
||||
response,
|
||||
"PSD 模板渲染失败",
|
||||
getFriendlyRateLimitMessage("当前 PSD 渲染请求过于频繁,请稍后再试")
|
||||
);
|
||||
if (!payload?.success || !payload.result?.url) {
|
||||
throw new Error(payload?.error || "PSD 模板渲染失败");
|
||||
}
|
||||
|
||||
setRenderedResultUrl(payload.result.url);
|
||||
@ -2168,7 +2236,11 @@ export function AgentPsdTemplateEditorDialog({
|
||||
: "模板替换结果已生成"
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "PSD 模板渲染失败");
|
||||
toast.error(
|
||||
getUserFacingRequestErrorMessage(error, "PSD 模板渲染失败", {
|
||||
rateLimitMessage: getFriendlyRateLimitMessage("当前 PSD 渲染请求过于频繁,请稍后再试"),
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setIsRendering(false);
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ import {
|
||||
isVideoModelDescriptor,
|
||||
} from "@/lib/ai/model-classification";
|
||||
import { getVideoToolOptionsForMode } from "@/lib/ai/video-tool-capability";
|
||||
import { getFriendlyRateLimitMessage, isRateLimitError, getUserFacingRequestErrorMessage } from "@/lib/request-error";
|
||||
import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils";
|
||||
import { Artifact } from "@/components/artifact";
|
||||
import { ProtocolDebugPanel } from "@/components/protocol-debug-panel";
|
||||
@ -373,6 +374,7 @@ export function Chat({
|
||||
const imagePlaceholderTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const latestAssistantDisplayTextRef = useRef<string>("");
|
||||
const startedGenerationMessageIdRef = useRef<string | null>(null);
|
||||
const lastRateLimitToastAtRef = useRef(0);
|
||||
|
||||
// 🆕 存储通过 data-appendMessage 添加的消息,避免与 useChat 内部状态冲突
|
||||
const [appendedMessages, setAppendedMessages] = useState<ChatMessage[]>([]);
|
||||
@ -948,6 +950,18 @@ export function Chat({
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedErrorMessage = getUserFacingRequestErrorMessage(
|
||||
{
|
||||
...(error instanceof Error ? error : {}),
|
||||
data: responseData,
|
||||
message: fallbackMessage,
|
||||
},
|
||||
"请求失败,请稍后重试",
|
||||
{
|
||||
rateLimitMessage: getFriendlyRateLimitMessage(),
|
||||
}
|
||||
);
|
||||
|
||||
if (error instanceof ChatSDKError) {
|
||||
// Check if it's a credit card error
|
||||
if (
|
||||
@ -955,10 +969,17 @@ export function Chat({
|
||||
) {
|
||||
setShowCreditCardAlert(true);
|
||||
} else {
|
||||
toast.error(error.message);
|
||||
toast.error(normalizedErrorMessage);
|
||||
}
|
||||
} else {
|
||||
toast.error(fallbackMessage || "请求失败,请稍后重试");
|
||||
if (isRateLimitError({ ...(error as any), data: responseData, message: fallbackMessage })) {
|
||||
const now = Date.now();
|
||||
if (now - lastRateLimitToastAtRef.current < 4000) {
|
||||
return;
|
||||
}
|
||||
lastRateLimitToastAtRef.current = now;
|
||||
}
|
||||
toast.error(normalizedErrorMessage);
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -1459,7 +1480,11 @@ export function Chat({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[变清晰] 错误:", error);
|
||||
toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试");
|
||||
toast.error(
|
||||
getUserFacingRequestErrorMessage(error, "变清晰失败,请稍后重试", {
|
||||
rateLimitMessage: getFriendlyRateLimitMessage("当前变清晰请求过于频繁,请稍后再试"),
|
||||
})
|
||||
);
|
||||
// 移除加载消息
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
} finally {
|
||||
@ -1479,7 +1504,11 @@ export function Chat({
|
||||
} catch (error) {
|
||||
console.error("[变清晰] 错误:", error);
|
||||
console.log("[变清晰] 外层 catch 执行,重置 isProcessingEnhance");
|
||||
toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试");
|
||||
toast.error(
|
||||
getUserFacingRequestErrorMessage(error, "变清晰失败,请稍后重试", {
|
||||
rateLimitMessage: getFriendlyRateLimitMessage("当前变清晰请求过于频繁,请稍后再试"),
|
||||
})
|
||||
);
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
setIsProcessingEnhance(false);
|
||||
}
|
||||
@ -1604,7 +1633,11 @@ export function Chat({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[抠图] 错误:", error);
|
||||
toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试");
|
||||
toast.error(
|
||||
getUserFacingRequestErrorMessage(error, "抠图失败,请稍后重试", {
|
||||
rateLimitMessage: getFriendlyRateLimitMessage("当前抠图请求过于频繁,请稍后再试"),
|
||||
})
|
||||
);
|
||||
// 移除加载消息
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
} finally {
|
||||
@ -1624,7 +1657,11 @@ export function Chat({
|
||||
} catch (error) {
|
||||
console.error("[抠图] 错误:", error);
|
||||
console.log("[抠图] 外层 catch 执行,重置 isProcessingRmbg");
|
||||
toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试");
|
||||
toast.error(
|
||||
getUserFacingRequestErrorMessage(error, "抠图失败,请稍后重试", {
|
||||
rateLimitMessage: getFriendlyRateLimitMessage("当前抠图请求过于频繁,请稍后再试"),
|
||||
})
|
||||
);
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
setIsProcessingRmbg(false);
|
||||
}
|
||||
@ -1715,7 +1752,11 @@ export function Chat({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[去水印] 错误:", error);
|
||||
toast.error(error instanceof Error ? error.message : "去水印失败,请稍后重试");
|
||||
toast.error(
|
||||
getUserFacingRequestErrorMessage(error, "去水印失败,请稍后重试", {
|
||||
rateLimitMessage: getFriendlyRateLimitMessage("当前去水印请求过于频繁,请稍后再试"),
|
||||
})
|
||||
);
|
||||
// 移除加载消息
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
} finally {
|
||||
@ -1844,7 +1885,11 @@ export function Chat({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[图像拓展] 错误:", error);
|
||||
toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试");
|
||||
toast.error(
|
||||
getUserFacingRequestErrorMessage(error, "图像拓展失败,请稍后重试", {
|
||||
rateLimitMessage: getFriendlyRateLimitMessage("当前图像拓展请求过于频繁,请稍后再试"),
|
||||
})
|
||||
);
|
||||
// 移除加载消息
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
} finally {
|
||||
@ -1864,7 +1909,11 @@ export function Chat({
|
||||
} catch (error) {
|
||||
console.error("[图像拓展] 错误:", error);
|
||||
console.log("[图像拓展] 外层 catch 执行,重置 isProcessingExtend");
|
||||
toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试");
|
||||
toast.error(
|
||||
getUserFacingRequestErrorMessage(error, "图像拓展失败,请稍后重试", {
|
||||
rateLimitMessage: getFriendlyRateLimitMessage("当前图像拓展请求过于频繁,请稍后再试"),
|
||||
})
|
||||
);
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
setIsProcessingExtend(false);
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ export function getMessageByErrorCode(errorCode: ErrorCode): string {
|
||||
return "Your account does not have access to this feature.";
|
||||
|
||||
case "rate_limit:chat":
|
||||
return "You have exceeded your maximum number of messages for the day. Please try again later.";
|
||||
return "今日消息次数已达上限,请稍后再试。";
|
||||
case "not_found:chat":
|
||||
return "The requested chat was not found. Please check the chat ID and try again.";
|
||||
case "forbidden:chat":
|
||||
|
||||
190
lib/request-error.ts
Normal file
190
lib/request-error.ts
Normal file
@ -0,0 +1,190 @@
|
||||
const RATE_LIMIT_PATTERNS = [
|
||||
/\btoo many requests\b/i,
|
||||
/\brate limit(?:ed|ing)?\b/i,
|
||||
/\bquota exceeded\b/i,
|
||||
/请求过于频繁/,
|
||||
/请求太频繁/,
|
||||
/访问过于频繁/,
|
||||
/频率过高/,
|
||||
/达到速率限制/,
|
||||
/限流/,
|
||||
];
|
||||
const JSON_EMBEDDED_MESSAGE_PATTERN = /^[["{]/;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function normalizeTextCandidate(value: string) {
|
||||
return value.trim().replace(/^["']|["']$/g, "");
|
||||
}
|
||||
|
||||
function collectTextCandidates(
|
||||
value: unknown,
|
||||
results: string[],
|
||||
seen = new WeakSet<object>(),
|
||||
depth = 0
|
||||
) {
|
||||
if (depth > 4 || value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const normalized = normalizeTextCandidate(value);
|
||||
if (normalized) {
|
||||
results.push(normalized);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
collectTextCandidates(item, results, seen, depth + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRecord(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (seen.has(value)) {
|
||||
return;
|
||||
}
|
||||
seen.add(value);
|
||||
|
||||
const preferredKeys = [
|
||||
"message",
|
||||
"error",
|
||||
"detail",
|
||||
"cause",
|
||||
"statusText",
|
||||
"responseText",
|
||||
"body",
|
||||
"data",
|
||||
];
|
||||
|
||||
for (const key of preferredKeys) {
|
||||
if (key in value) {
|
||||
collectTextCandidates(value[key], results, seen, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, candidate] of Object.entries(value)) {
|
||||
if (preferredKeys.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
collectTextCandidates(candidate, results, seen, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonEmbeddedMessage(value: string): string | null {
|
||||
const normalized = normalizeTextCandidate(value);
|
||||
if (!normalized || !JSON_EMBEDDED_MESSAGE_PATTERN.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(normalized);
|
||||
const messages: string[] = [];
|
||||
collectTextCandidates(parsed, messages);
|
||||
return messages.find(Boolean) || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getRequestErrorStatus(error: unknown): number | null {
|
||||
const candidates: unknown[] = [];
|
||||
|
||||
if (isRecord(error)) {
|
||||
candidates.push(
|
||||
error.status,
|
||||
error.statusCode,
|
||||
isRecord(error.response) ? error.response.status : null,
|
||||
isRecord(error.cause) ? error.cause.status : null,
|
||||
isRecord(error.data) ? error.data.status : null,
|
||||
isRecord(error.data) ? error.data.statusCode : null
|
||||
);
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const numeric = Number(candidate);
|
||||
if (Number.isFinite(numeric) && numeric >= 100) {
|
||||
return Math.round(numeric);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getRequestErrorMessage(error: unknown): string | null {
|
||||
const candidates: string[] = [];
|
||||
|
||||
if (error instanceof Error) {
|
||||
collectTextCandidates(
|
||||
{
|
||||
message: error.message,
|
||||
cause: error.cause,
|
||||
...(isRecord(error) ? error : {}),
|
||||
},
|
||||
candidates
|
||||
);
|
||||
} else {
|
||||
collectTextCandidates(error, candidates);
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const embeddedMessage = parseJsonEmbeddedMessage(candidate);
|
||||
const normalized = normalizeTextCandidate(embeddedMessage || candidate);
|
||||
if (normalized && normalized !== "[object Object]") {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isRateLimitError(error: unknown): boolean {
|
||||
if (getRequestErrorStatus(error) === 429) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const message = getRequestErrorMessage(error);
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(message));
|
||||
}
|
||||
|
||||
export function getFriendlyRateLimitMessage(
|
||||
customMessage = "当前请求过于频繁,请稍后再试"
|
||||
) {
|
||||
return (
|
||||
normalizeTextCandidate(customMessage) || "当前请求过于频繁,请稍后再试"
|
||||
);
|
||||
}
|
||||
|
||||
export function getUserFacingRequestErrorMessage(
|
||||
error: unknown,
|
||||
fallbackMessage = "请求失败,请稍后重试",
|
||||
options?: {
|
||||
rateLimitMessage?: string;
|
||||
}
|
||||
) {
|
||||
if (isRateLimitError(error)) {
|
||||
return getFriendlyRateLimitMessage(options?.rateLimitMessage);
|
||||
}
|
||||
|
||||
const message = getRequestErrorMessage(error);
|
||||
if (!message) {
|
||||
return fallbackMessage;
|
||||
}
|
||||
|
||||
if (RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(message))) {
|
||||
return getFriendlyRateLimitMessage(options?.rateLimitMessage);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
34
lib/utils.ts
34
lib/utils.ts
@ -9,6 +9,7 @@ import { formatISO } from 'date-fns';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { DBMessage, Document } from '@/lib/db/schema';
|
||||
import { ChatSDKError, type ErrorCode } from './errors';
|
||||
import { getFriendlyRateLimitMessage, getUserFacingRequestErrorMessage } from './request-error';
|
||||
import type { ChatMessage, ChatTools, CustomUIDataTypes } from './types';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
@ -39,12 +40,21 @@ export async function fetchWithErrorHandlers(
|
||||
|
||||
if (!response.ok) {
|
||||
let payload: any = null;
|
||||
let responseText = "";
|
||||
try {
|
||||
payload = await response.json();
|
||||
payload = await response.clone().json();
|
||||
} catch {
|
||||
payload = null;
|
||||
}
|
||||
|
||||
if (payload == null) {
|
||||
try {
|
||||
responseText = (await response.text()).trim();
|
||||
} catch {
|
||||
responseText = "";
|
||||
}
|
||||
}
|
||||
|
||||
const code =
|
||||
typeof payload?.code === "string" && payload.code.includes(":")
|
||||
? (payload.code as ErrorCode)
|
||||
@ -60,16 +70,30 @@ export async function fetchWithErrorHandlers(
|
||||
}
|
||||
|
||||
const fallbackMessage =
|
||||
(typeof payload?.message === "string" && payload.message) ||
|
||||
(typeof payload?.error === "string" && payload.error) ||
|
||||
`Request failed with status ${response.status}`;
|
||||
getUserFacingRequestErrorMessage(
|
||||
{
|
||||
status: response.status,
|
||||
data: payload,
|
||||
message: responseText,
|
||||
},
|
||||
`Request failed with status ${response.status}`,
|
||||
{
|
||||
rateLimitMessage: getFriendlyRateLimitMessage(),
|
||||
}
|
||||
);
|
||||
const requestError = new Error(fallbackMessage) as Error & {
|
||||
status?: number;
|
||||
data?: any;
|
||||
};
|
||||
requestError.name = "RequestError";
|
||||
requestError.status = response.status;
|
||||
requestError.data = payload;
|
||||
requestError.data =
|
||||
payload ??
|
||||
(responseText
|
||||
? {
|
||||
message: responseText,
|
||||
}
|
||||
: null);
|
||||
throw requestError;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user