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,
|
ResolvedTargetAssets,
|
||||||
} from "@/lib/ai/context-memory/types";
|
} from "@/lib/ai/context-memory/types";
|
||||||
import { ChatSDKError } from "@/lib/errors";
|
import { ChatSDKError } from "@/lib/errors";
|
||||||
|
import {
|
||||||
|
getFriendlyRateLimitMessage,
|
||||||
|
getRequestErrorMessage,
|
||||||
|
isRateLimitError,
|
||||||
|
} from "@/lib/request-error";
|
||||||
import { extractImageUrlsFromText } from "@/lib/image-url";
|
import { extractImageUrlsFromText } from "@/lib/image-url";
|
||||||
import {
|
import {
|
||||||
resolveClarificationBypass,
|
resolveClarificationBypass,
|
||||||
@ -12637,6 +12642,28 @@ export async function POST(request: Request) {
|
|||||||
return error.toResponse();
|
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
|
// Check for Vercel AI Gateway credit card error
|
||||||
if (
|
if (
|
||||||
error instanceof Error &&
|
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 { auth } from "@/app/(auth)/auth";
|
||||||
|
import { ExtendServiceError, executeExtendJob } from "@/lib/ai/extend/service";
|
||||||
import {
|
import {
|
||||||
executeExtendJob,
|
getFriendlyRateLimitMessage,
|
||||||
ExtendServiceError,
|
getUserFacingRequestErrorMessage,
|
||||||
} from "@/lib/ai/extend/service";
|
isRateLimitError,
|
||||||
|
} from "@/lib/request-error";
|
||||||
|
|
||||||
export const maxDuration = 180;
|
export const maxDuration = 180;
|
||||||
|
|
||||||
@ -89,9 +91,25 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ExtendServiceError) {
|
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(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: error.message,
|
error: getUserFacingRequestErrorMessage(error, "图像拓展失败"),
|
||||||
detail: error.detail || error.message,
|
detail: error.detail || error.message,
|
||||||
},
|
},
|
||||||
{ status: error.status },
|
{ status: error.status },
|
||||||
@ -99,9 +117,24 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.error("[Extend API] 错误:", error);
|
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(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: "图像拓展失败",
|
error: getUserFacingRequestErrorMessage(error, "图像拓展失败"),
|
||||||
detail: error instanceof Error ? error.message : String(error),
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
},
|
},
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
|
|||||||
@ -1,22 +1,27 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
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 { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
||||||
import { normalizeAgentPsdImagePlacement } from "@/lib/agents/psd-image-placement";
|
import { normalizeAgentPsdImagePlacement } from "@/lib/agents/psd-image-placement";
|
||||||
import { describeRemoteAssetLocation } from "@/lib/agents/psd-remote-asset";
|
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 { 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 { 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";
|
export const runtime = "nodejs";
|
||||||
|
|
||||||
@ -163,10 +168,26 @@ export async function POST(request: NextRequest) {
|
|||||||
assignedImageCount: Object.keys(body.imageAssignments || {}).length,
|
assignedImageCount: Object.keys(body.imageAssignments || {}).length,
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isRateLimitError(error)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: getFriendlyRateLimitMessage("当前操作过于频繁,请稍后再试"),
|
||||||
|
} satisfies AgentPsdTemplateRenderResponse,
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
headers: {
|
||||||
|
"Retry-After": "10",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error && String(error.message || "").trim()
|
getUserFacingRequestErrorMessage(error, "PSD 模板渲染失败", {
|
||||||
? error.message
|
rateLimitMessage: getFriendlyRateLimitMessage("当前操作过于频繁,请稍后再试"),
|
||||||
: "PSD 模板渲染失败";
|
});
|
||||||
return jsonError(502, message);
|
return jsonError(502, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,6 +81,7 @@ import {
|
|||||||
resolveTextLayout,
|
resolveTextLayout,
|
||||||
} from "@/lib/agents/psd-text-layout";
|
} from "@/lib/agents/psd-text-layout";
|
||||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||||
|
import { getFriendlyRateLimitMessage, getUserFacingRequestErrorMessage } from "@/lib/request-error";
|
||||||
|
|
||||||
function buildTemplateListRequestUrl(params?: {
|
function buildTemplateListRequestUrl(params?: {
|
||||||
brandName?: string | null;
|
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_VIEWPORT_PADDING = 56;
|
||||||
const PREVIEW_STAGE_MAX_SCALE = 0.82;
|
const PREVIEW_STAGE_MAX_SCALE = 0.82;
|
||||||
const PREVIEW_STAGE_TARGET_WIDTH = 760;
|
const PREVIEW_STAGE_TARGET_WIDTH = 760;
|
||||||
@ -1946,15 +1996,20 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.set("file", file);
|
formData.set("file", file);
|
||||||
const response = await fetchUploadWithRetry(formData);
|
const response = await fetchUploadWithRetry(formData);
|
||||||
const payload = await response.json();
|
const payload = await parseActionJsonResponse<{ url?: string }>(
|
||||||
|
response,
|
||||||
|
"素材上传失败",
|
||||||
|
getFriendlyRateLimitMessage("当前上传过于频繁,请稍后再试")
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok || !payload.url) {
|
if (!payload?.url) {
|
||||||
throw new Error(payload.error || "图片上传失败");
|
throw new Error("图片上传失败");
|
||||||
}
|
}
|
||||||
|
const uploadedImageUrl = payload.url;
|
||||||
|
|
||||||
setImageAssignments((current) => ({
|
setImageAssignments((current) => ({
|
||||||
...current,
|
...current,
|
||||||
[slotId]: payload.url,
|
[slotId]: uploadedImageUrl,
|
||||||
}));
|
}));
|
||||||
setImagePlacements((current) => ({
|
setImagePlacements((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@ -1967,7 +2022,11 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
});
|
});
|
||||||
toast.success("替换图片已上传");
|
toast.success("替换图片已上传");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "素材上传失败");
|
toast.error(
|
||||||
|
getUserFacingRequestErrorMessage(error, "素材上传失败", {
|
||||||
|
rateLimitMessage: getFriendlyRateLimitMessage("当前上传过于频繁,请稍后再试"),
|
||||||
|
})
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setUploadingSlotId(null);
|
setUploadingSlotId(null);
|
||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
@ -1993,13 +2052,14 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
prompt: "PSD 套版图片拓展处理",
|
prompt: "PSD 套版图片拓展处理",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const payload = await response.json().catch(() => null);
|
const payload = await parseActionJsonResponse<{
|
||||||
if (!response.ok) {
|
original_url?: string;
|
||||||
throw new Error(
|
thumbnail_url?: string;
|
||||||
String(payload?.error || payload?.detail || "").trim() ||
|
}>(
|
||||||
"图片拓展失败"
|
response,
|
||||||
);
|
"图片拓展失败",
|
||||||
}
|
getFriendlyRateLimitMessage("当前图片拓展请求过于频繁,请稍后再试")
|
||||||
|
);
|
||||||
|
|
||||||
const nextImageUrl = String(
|
const nextImageUrl = String(
|
||||||
payload?.original_url || payload?.thumbnail_url || ""
|
payload?.original_url || payload?.thumbnail_url || ""
|
||||||
@ -2028,7 +2088,11 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
: "图片拓展完成,已替换当前槽位"
|
: "图片拓展完成,已替换当前槽位"
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "图片拓展失败");
|
toast.error(
|
||||||
|
getUserFacingRequestErrorMessage(error, "图片拓展失败", {
|
||||||
|
rateLimitMessage: getFriendlyRateLimitMessage("当前图片拓展请求过于频繁,请稍后再试"),
|
||||||
|
})
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setExtendingSlotId(null);
|
setExtendingSlotId(null);
|
||||||
}
|
}
|
||||||
@ -2154,9 +2218,13 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = (await response.json()) as AgentPsdTemplateRenderResponse;
|
const payload = await parseActionJsonResponse<AgentPsdTemplateRenderResponse>(
|
||||||
if (!response.ok || !payload.success || !payload.result?.url) {
|
response,
|
||||||
throw new Error(payload.error || "PSD 模板渲染失败");
|
"PSD 模板渲染失败",
|
||||||
|
getFriendlyRateLimitMessage("当前 PSD 渲染请求过于频繁,请稍后再试")
|
||||||
|
);
|
||||||
|
if (!payload?.success || !payload.result?.url) {
|
||||||
|
throw new Error(payload?.error || "PSD 模板渲染失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
setRenderedResultUrl(payload.result.url);
|
setRenderedResultUrl(payload.result.url);
|
||||||
@ -2168,7 +2236,11 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
: "模板替换结果已生成"
|
: "模板替换结果已生成"
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "PSD 模板渲染失败");
|
toast.error(
|
||||||
|
getUserFacingRequestErrorMessage(error, "PSD 模板渲染失败", {
|
||||||
|
rateLimitMessage: getFriendlyRateLimitMessage("当前 PSD 渲染请求过于频繁,请稍后再试"),
|
||||||
|
})
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsRendering(false);
|
setIsRendering(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import {
|
|||||||
isVideoModelDescriptor,
|
isVideoModelDescriptor,
|
||||||
} from "@/lib/ai/model-classification";
|
} from "@/lib/ai/model-classification";
|
||||||
import { getVideoToolOptionsForMode } from "@/lib/ai/video-tool-capability";
|
import { getVideoToolOptionsForMode } from "@/lib/ai/video-tool-capability";
|
||||||
|
import { getFriendlyRateLimitMessage, isRateLimitError, getUserFacingRequestErrorMessage } from "@/lib/request-error";
|
||||||
import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils";
|
import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils";
|
||||||
import { Artifact } from "@/components/artifact";
|
import { Artifact } from "@/components/artifact";
|
||||||
import { ProtocolDebugPanel } from "@/components/protocol-debug-panel";
|
import { ProtocolDebugPanel } from "@/components/protocol-debug-panel";
|
||||||
@ -373,6 +374,7 @@ export function Chat({
|
|||||||
const imagePlaceholderTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const imagePlaceholderTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const latestAssistantDisplayTextRef = useRef<string>("");
|
const latestAssistantDisplayTextRef = useRef<string>("");
|
||||||
const startedGenerationMessageIdRef = useRef<string | null>(null);
|
const startedGenerationMessageIdRef = useRef<string | null>(null);
|
||||||
|
const lastRateLimitToastAtRef = useRef(0);
|
||||||
|
|
||||||
// 🆕 存储通过 data-appendMessage 添加的消息,避免与 useChat 内部状态冲突
|
// 🆕 存储通过 data-appendMessage 添加的消息,避免与 useChat 内部状态冲突
|
||||||
const [appendedMessages, setAppendedMessages] = useState<ChatMessage[]>([]);
|
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) {
|
if (error instanceof ChatSDKError) {
|
||||||
// Check if it's a credit card error
|
// Check if it's a credit card error
|
||||||
if (
|
if (
|
||||||
@ -955,10 +969,17 @@ export function Chat({
|
|||||||
) {
|
) {
|
||||||
setShowCreditCardAlert(true);
|
setShowCreditCardAlert(true);
|
||||||
} else {
|
} else {
|
||||||
toast.error(error.message);
|
toast.error(normalizedErrorMessage);
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error("[变清晰] 错误:", 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));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
} finally {
|
} finally {
|
||||||
@ -1479,7 +1504,11 @@ export function Chat({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[变清晰] 错误:", error);
|
console.error("[变清晰] 错误:", error);
|
||||||
console.log("[变清晰] 外层 catch 执行,重置 isProcessingEnhance");
|
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));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
setIsProcessingEnhance(false);
|
setIsProcessingEnhance(false);
|
||||||
}
|
}
|
||||||
@ -1604,7 +1633,11 @@ export function Chat({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[抠图] 错误:", 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));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
} finally {
|
} finally {
|
||||||
@ -1624,7 +1657,11 @@ export function Chat({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[抠图] 错误:", error);
|
console.error("[抠图] 错误:", error);
|
||||||
console.log("[抠图] 外层 catch 执行,重置 isProcessingRmbg");
|
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));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
setIsProcessingRmbg(false);
|
setIsProcessingRmbg(false);
|
||||||
}
|
}
|
||||||
@ -1715,7 +1752,11 @@ export function Chat({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[去水印] 错误:", 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));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
} finally {
|
} finally {
|
||||||
@ -1844,7 +1885,11 @@ export function Chat({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[图像拓展] 错误:", 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));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
} finally {
|
} finally {
|
||||||
@ -1864,7 +1909,11 @@ export function Chat({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[图像拓展] 错误:", error);
|
console.error("[图像拓展] 错误:", error);
|
||||||
console.log("[图像拓展] 外层 catch 执行,重置 isProcessingExtend");
|
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));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
setIsProcessingExtend(false);
|
setIsProcessingExtend(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,7 +97,7 @@ export function getMessageByErrorCode(errorCode: ErrorCode): string {
|
|||||||
return "Your account does not have access to this feature.";
|
return "Your account does not have access to this feature.";
|
||||||
|
|
||||||
case "rate_limit:chat":
|
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":
|
case "not_found:chat":
|
||||||
return "The requested chat was not found. Please check the chat ID and try again.";
|
return "The requested chat was not found. Please check the chat ID and try again.";
|
||||||
case "forbidden:chat":
|
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 { twMerge } from 'tailwind-merge';
|
||||||
import type { DBMessage, Document } from '@/lib/db/schema';
|
import type { DBMessage, Document } from '@/lib/db/schema';
|
||||||
import { ChatSDKError, type ErrorCode } from './errors';
|
import { ChatSDKError, type ErrorCode } from './errors';
|
||||||
|
import { getFriendlyRateLimitMessage, getUserFacingRequestErrorMessage } from './request-error';
|
||||||
import type { ChatMessage, ChatTools, CustomUIDataTypes } from './types';
|
import type { ChatMessage, ChatTools, CustomUIDataTypes } from './types';
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
@ -39,12 +40,21 @@ export async function fetchWithErrorHandlers(
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let payload: any = null;
|
let payload: any = null;
|
||||||
|
let responseText = "";
|
||||||
try {
|
try {
|
||||||
payload = await response.json();
|
payload = await response.clone().json();
|
||||||
} catch {
|
} catch {
|
||||||
payload = null;
|
payload = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (payload == null) {
|
||||||
|
try {
|
||||||
|
responseText = (await response.text()).trim();
|
||||||
|
} catch {
|
||||||
|
responseText = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const code =
|
const code =
|
||||||
typeof payload?.code === "string" && payload.code.includes(":")
|
typeof payload?.code === "string" && payload.code.includes(":")
|
||||||
? (payload.code as ErrorCode)
|
? (payload.code as ErrorCode)
|
||||||
@ -60,16 +70,30 @@ export async function fetchWithErrorHandlers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fallbackMessage =
|
const fallbackMessage =
|
||||||
(typeof payload?.message === "string" && payload.message) ||
|
getUserFacingRequestErrorMessage(
|
||||||
(typeof payload?.error === "string" && payload.error) ||
|
{
|
||||||
`Request failed with status ${response.status}`;
|
status: response.status,
|
||||||
|
data: payload,
|
||||||
|
message: responseText,
|
||||||
|
},
|
||||||
|
`Request failed with status ${response.status}`,
|
||||||
|
{
|
||||||
|
rateLimitMessage: getFriendlyRateLimitMessage(),
|
||||||
|
}
|
||||||
|
);
|
||||||
const requestError = new Error(fallbackMessage) as Error & {
|
const requestError = new Error(fallbackMessage) as Error & {
|
||||||
status?: number;
|
status?: number;
|
||||||
data?: any;
|
data?: any;
|
||||||
};
|
};
|
||||||
requestError.name = "RequestError";
|
requestError.name = "RequestError";
|
||||||
requestError.status = response.status;
|
requestError.status = response.status;
|
||||||
requestError.data = payload;
|
requestError.data =
|
||||||
|
payload ??
|
||||||
|
(responseText
|
||||||
|
? {
|
||||||
|
message: responseText,
|
||||||
|
}
|
||||||
|
: null);
|
||||||
throw requestError;
|
throw requestError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user