ai-chatbot/lib/request-error.ts

278 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const RATE_LIMIT_PATTERNS = [
/\btoo many requests\b/i,
/\brate limit(?:ed|ing)?\b/i,
/\bquota exceeded\b/i,
/请求过于频繁/,
/请求太频繁/,
/访问过于频繁/,
/频率过高/,
/达到速率限制/,
/限流/,
];
const PROVIDER_BALANCE_PATTERNS = [
/余额(?:不足|不够|已?耗尽)/i,
/额度(?:不足|不够|已?耗尽)/i,
/预扣费额度(?:不足|失败)/i,
/\binsufficient[ _-]?(?:balance|credits?|funds?|quota)\b/i,
/\b(?:balance|credits?|funds?)\s+(?:is\s+)?(?:not enough|insufficient|exhausted)\b/i,
/\b(?:exceeded_current_quota|pre_consume_token_quota_failed)\b/i,
/\btoken quota is not enough\b/i,
/\bquota is not enough\b/i,
/\b(?:not enough credits?|credits? is not enough)\b/i,
/\b(?:billing_hard_limit_reached|payment_required)\b/i,
];
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<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;
}
function getRequestErrorText(error: unknown) {
const candidates: string[] = [];
if (error instanceof Error) {
collectTextCandidates({ ...error, message: error.message, cause: error.cause }, candidates);
} else {
collectTextCandidates(error, candidates);
}
return candidates.join(" ").slice(0, 4000);
}
export function getGatewayErrorPageMessage(content: string): string | null {
if (!/^\s*(?:<!doctype\s+html\b|<html\b)/i.test(content)) return null;
if (!/\bcf-error-details\b/i.test(content)) return null;
if (/\b524\b/.test(content)) {
return "模型服务响应超时(HTTP 524),生成结果尚未确认。请先查看历史记录,避免重复提交。";
}
return "模型服务暂时不可用,请稍后再试或联系管理员。";
}
export function isGatewayResponseTimeout(error: unknown): boolean {
return getRequestErrorStatus(error) === 524 ||
/\bHTTP\s*524\b|\b524\s*:\s*A timeout occurred\b|Error code\s*524\b/i.test(getRequestErrorText(error));
}
function getPublicGatewayErrorMessage(error: unknown): string | null {
if (isGatewayResponseTimeout(error)) {
return "模型服务响应超时(HTTP 524),生成结果尚未确认。请先查看历史记录,避免重复提交。";
}
const message = getRequestErrorMessage(error);
return message ? getGatewayErrorPageMessage(message) : null;
}
export function isProviderBalanceError(error: unknown): boolean {
const message = getRequestErrorText(error);
return PROVIDER_BALANCE_PATTERNS.some((pattern) => pattern.test(message));
}
export function isRateLimitError(error: unknown): boolean {
// Some providers return HTTP 429 for an actual unpaid balance. The status
// alone must not override an explicit billing error in the response body.
if (isProviderBalanceError(error)) {
return false;
}
if (getRequestErrorStatus(error) === 429) {
return true;
}
const message = getRequestErrorText(error);
if (!message) {
return false;
}
return /(?:^|\D)429(?:\D|$)/.test(message) ||
RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(message));
}
export function getUserFacingImageGenerationErrorMessage(
error: unknown,
fallbackMessage = "图片生成失败,请稍后重试",
) {
const gatewayMessage = getPublicGatewayErrorMessage(error);
if (gatewayMessage) return gatewayMessage;
if (isProviderBalanceError(error)) {
return "图片服务商线路余额或额度不足,请切换模型或联系管理员;与您的积分余额无关。";
}
if (isRateLimitError(error)) {
return "图片服务请求过于频繁,请稍后重试或切换模型。";
}
return getRequestErrorMessage(error) || fallbackMessage;
}
export function getFriendlyRateLimitMessage(
customMessage = "当前请求过于频繁,请稍后再试"
) {
return (
normalizeTextCandidate(customMessage) || "当前请求过于频繁,请稍后再试"
);
}
export function getUserFacingRequestErrorMessage(
error: unknown,
fallbackMessage = "请求失败,请稍后重试",
options?: {
rateLimitMessage?: string;
}
) {
const gatewayMessage = getPublicGatewayErrorMessage(error);
if (gatewayMessage) return gatewayMessage;
if (isProviderBalanceError(error)) {
return "模型服务商线路余额或额度不足,请切换模型或联系管理员。";
}
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);
}
if (NETWORK_TRANSPORT_PATTERNS.some((pattern) => pattern.test(message))) {
return "请求发送失败,请检查网络或稍后重试。";
}
return message;
}