2026-05-26 22:15:49 +08:00

189 lines
5.0 KiB
TypeScript

import { NextResponse } from "next/server";
import { getRequestErrorMessage } from "@/lib/request-error";
import { generateUUID } from "@/lib/utils";
const DEFAULT_CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS = 55_000;
export class OperationTimeoutError extends Error {
constructor(operation: string, timeoutMs: number) {
super(`${operation} timed out after ${timeoutMs}ms`);
this.name = "OperationTimeoutError";
}
}
export 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 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,
};
}
export 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 }
);
}
export async function withOperationTimeout<T>(
operation: Promise<T>,
operationName: string,
timeoutMs: number
): Promise<T> {
let timer: ReturnType<typeof setTimeout> | null = null;
try {
return await Promise.race([
operation,
new Promise<never>((_, reject) => {
timer = setTimeout(
() => reject(new OperationTimeoutError(operationName, timeoutMs)),
timeoutMs
);
}),
]);
} finally {
if (timer) {
clearTimeout(timer);
}
}
}
export function getRemainingBudgetMs(startedAt: number, budgetMs: number) {
return Math.max(0, budgetMs - (Date.now() - startedAt));
}
export function getBudgetedTimeout(params: {
startedAt: number;
preferredMs: number;
minimumMs?: number;
budgetMs?: number;
}) {
const minimumMs = params.minimumMs ?? 6_000;
const remainingMs = getRemainingBudgetMs(
params.startedAt,
params.budgetMs ?? DEFAULT_CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS
);
if (remainingMs < minimumMs) {
return null;
}
return Math.min(params.preferredMs, remainingMs);
}