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 getGatewayErrorPageMessage(content: string): string | null { if (!/^\s*(?: 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 type RequestFailureDetails = { code: string; message: string; recommendation: string; outcome: "failed" | "unknown"; }; export function getRequestFailureDetails( error: unknown, fallbackMessage = "请求未能完成", ): RequestFailureDetails { const text = getRequestErrorText(error); const status = getRequestErrorStatus(error); const detail = (code: string, message: string, recommendation: string, outcome: "failed" | "unknown" = "failed") => ({ code, message, recommendation, outcome }); if (isGatewayResponseTimeout(error)) { return detail("RESULT_UNCONFIRMED", "模型服务响应超时(HTTP 524),生成结果尚未确认。", "请先查看历史记录,避免重复提交。", "unknown"); } if ((isRecord(error) && error.providerOutcomeUnknown === true) || /结果(?:尚)?未确认|结果暂未确认|request_outcome_unknown|任务执行中断|may still be running/i.test(text)) { return detail("RESULT_UNCONFIRMED", "任务结果尚未确认,可能仍在处理。", "请先查看原任务或历史记录;持续没有结果时联系管理员,避免重复生成。", "unknown"); } if (isProviderBalanceError(error)) { return detail("PROVIDER_BALANCE_EXHAUSTED", "图片服务商线路余额或额度不足,与您的积分余额无关。", "请切换可用模型或联系管理员处理线路额度。"); } if (isRateLimitError(error)) { return detail("RATE_LIMITED", "当前请求过于频繁。", "请稍后重试,避免连续点击;模型线路限流时可切换其他可用模型。"); } if (/content.?polic|moderation|safety|policy_rejected|内容(?:敏感|审核|限制)|不能生成像那样|不能生成那样|违规/i.test(text)) { return detail("CONTENT_POLICY_REJECTED", "模型服务因内容审核限制拒绝了本次请求。", "请调整描述或参考图中的受限内容后再提交;重复提交相同内容通常无法解决。"); } if (/上传到存储桶失败|存储.*失败|归档.*失败|archive.*failed|storage.*failed/i.test(text)) { return detail("RESULT_STORAGE_FAILED", "处理结果保存失败,生成结果可能已经存在。", "请保留当前页面并联系管理员提供请求编号,先不要重复生成。", "unknown"); } if (/failed to download (?:the )?image url|failed to download.*reference.*image/i.test(text)) { return detail("PROVIDER_REFERENCE_FETCH_FAILED", "模型服务未能读取参考图。", "请重新上传参考图或切换模型后再提交;若原图仍可打开,请联系管理员检查模型取图线路,并提供请求编号。"); } if (/参考图.*(?:下载失败|不可|失效)|无法获取图片|图片(?:源文件|链接).*不可|获取图片.*失败|get image url failed/i.test(text)) { return detail("SOURCE_IMAGE_UNAVAILABLE", "请求中的图片无法读取,链接可能已失效或没有访问权限。", "请重新上传原图后再提交;若原图仍可打开,请联系管理员检查取图服务。"); } if (/版本冲突|画布.*(?:更新|过期)|CANVAS_REVISION_CONFLICT|stale.canvas/i.test(text)) { return detail("CANVAS_REVISION_CONFLICT", "画布已更新,本次保存与当前版本冲突。", "请先重新同步画布,再继续操作,避免覆盖较新的修改。"); } if (/no_eligible_route|service_unavailable|service_rejected|channel.*unavailable|暂无可用服务|未配置.*模型|没有可用.*(?:模型|通道)|通道.*(?:不可用|未通过)|鉴权/i.test(text)) { return detail("PROVIDER_UNAVAILABLE", "当前模型服务或通道暂不可用。", "请切换其他可用模型;若多个模型都失败,请联系管理员检查服务配置。"); } if (/neither a task ID nor output URLs|已完成.*未取回|completed without.*output/i.test(text)) { return detail("RESULT_UNCONFIRMED", "模型服务没有返回可读取的任务结果,生成状态尚未确认。", "请保留当前任务并联系管理员核对,避免重复生成。", "unknown"); } if (/(?:抠图|去水印|图片增强|图片|扩图)处理失败/.test(text)) { return detail("PROCESSING_FAILED", "图片处理服务执行失败,未返回具体原因。", "请联系管理员并提供请求编号,先核对原任务结果,避免重复处理。", "unknown"); } if (/service_error|upstream_server_error|excessive system load|generate(?: image)? failed/i.test(text)) { return detail("PROVIDER_EXECUTION_FAILED", "模型服务未能完成本次处理。", "请稍后重试;若连续失败,请切换模型或联系管理员,并提供请求编号。"); } if (/timeout|timed out|超时|fetch failed|ECONNRESET|ECONNREFUSED|failed to fetch|networkerror|load failed|network request failed/i.test(text) || [502, 504].includes(status || 0)) { return detail("RESULT_UNCONFIRMED", "请求连接中断或等待超时,任务结果尚未确认。", "请先查看原任务或历史记录,避免重复提交;持续没有结果时联系管理员。", "unknown"); } if (/generate(?: image)? failed|generation failed|upstream_server_error|service_error|excessive system load|未返回可用图片|任务FAILED/i.test(text) || (status !== null && status >= 500)) { return detail("PROVIDER_EXECUTION_FAILED", "模型服务未能完成本次处理。", "请稍后重试;若连续失败,请切换模型或联系管理员,并提供请求编号。"); } const message = getRequestErrorMessage(error); return detail("REQUEST_FAILED", message && !/^\s* pattern.test(message))) { return getFriendlyRateLimitMessage(options?.rateLimitMessage); } if (NETWORK_TRANSPORT_PATTERNS.some((pattern) => pattern.test(message))) { return "请求发送失败,请检查网络或稍后重试。"; } return message; }