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 { return typeof value === "object" && value !== null; } function normalizeTextCandidate(value: string) { return value.trim().replace(/^["']|["']$/g, ""); } function collectTextCandidates( value: unknown, results: string[], seen = new WeakSet(), 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 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 = "图片生成失败,请稍后重试", ) { 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; } ) { 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; }