cdcb032e45
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 27s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m28s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m39s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m12s
AI Code Review / AI Code Review (pull_request) Failing after 6m15s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check push changed paths (push) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 21s
CI/CD Pipeline / Build Staging API Image (push) Successful in 21s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 22s
CI/CD Pipeline / Integration Tests (push) Successful in 1m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m30s
CI/CD Pipeline / Validate - Style (push) Successful in 2m20s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m19s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m14s
CI/CD Pipeline / Unit Tests (push) Successful in 8m4s
CI/CD Pipeline / Validate - Security (push) Successful in 16m53s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 255h51m59s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 255h52m3s
CI/CD Pipeline / Build Production Web Image (push) Failing after 255h52m3s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 256h8m32s
CI/CD Pipeline / Deploy Production (push) Failing after 255h51m59s
CI/CD Pipeline / Frontend Lint (push) Failing after 256h8m58s
CI/CD Pipeline / Build Production API Image (push) Failing after 255h52m3s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 256h6m56s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 256h6m56s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 256h27m48s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 256h29m25s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 256h29m30s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 256h29m31s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 256h29m25s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 256h29m34s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 256h29m35s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 256h29m36s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 256h29m35s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 256h29m37s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 256h31m30s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 256h31m30s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 256h31m30s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 256h31m33s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 256h31m34s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 256h31m34s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 256h31m35s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 256h8m58s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 256h31m35s
CI/CD Pipeline / PR Build API Image (push) Failing after 256h8m58s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 256h31m37s
CI/CD Pipeline / CI Gate (push) Failing after 256h26m38s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 256h43m7s
CI/CD Pipeline / PR Build Web Image (push) Failing after 256h43m32s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 256h43m36s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 256h41m30s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 256h43m6s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 257h4m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 257h4m8s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 257h4m10s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 257h6m5s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 257h6m8s
133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
/**
|
||
* 统一错误信息提取
|
||
* 把 axios 错误(后端 detail / FastAPI 校验错误 / HTTP 状态码)、XHR/OSS 错误、
|
||
* 网络/超时错误、普通 Error 统一转成「可直接展示给用户」的中文信息。
|
||
*
|
||
* 与 api/client.ts 响应拦截器的提示口径保持一致;拦截器负责全局 toast,
|
||
* 页面/队列卡片用本工具把真实原因展示在持久位置(回调页、失败卡片等)。
|
||
*/
|
||
import type { AxiosError } from "axios"
|
||
|
||
/** 后端错误响应体可能出现的字段(FastAPI:detail;历史接口:message/msg) */
|
||
interface ErrorBody {
|
||
detail?: unknown
|
||
message?: unknown
|
||
msg?: unknown
|
||
}
|
||
|
||
/** FastAPI 422 校验错误单项 */
|
||
interface ValidationItem {
|
||
loc?: (string | number)[]
|
||
msg?: string
|
||
}
|
||
|
||
/** 从后端响应体提取人类可读信息(detail 可能是字符串、对象、422 数组) */
|
||
function extractBodyMessage(data: unknown): string {
|
||
if (!data || typeof data !== "object") return ""
|
||
const body = data as ErrorBody
|
||
|
||
const walk = (val: unknown): string => {
|
||
if (typeof val === "string") return val
|
||
if (Array.isArray(val)) {
|
||
// FastAPI 422: [{loc, msg, type}, ...] → 取每条 msg 拼接
|
||
const parts = val
|
||
.map((item) => {
|
||
if (typeof item === "string") return item
|
||
if (item && typeof item === "object") {
|
||
const v = item as ValidationItem
|
||
if (typeof v.msg === "string") {
|
||
const field = Array.isArray(v.loc) ? v.loc.filter((x) => x !== "body").join(".") : ""
|
||
return field ? `${field}: ${v.msg}` : v.msg
|
||
}
|
||
return walk(item)
|
||
}
|
||
return ""
|
||
})
|
||
.filter(Boolean)
|
||
return parts.join(";")
|
||
}
|
||
if (val && typeof val === "object") {
|
||
const obj = val as Record<string, unknown>
|
||
if (typeof obj.message === "string") return obj.message
|
||
if (typeof obj.msg === "string") return obj.msg
|
||
if (typeof obj.detail === "string") return obj.detail
|
||
if (obj.message && typeof obj.message === "object") return walk(obj.message)
|
||
if (obj.msg && typeof obj.msg === "object") return walk(obj.msg)
|
||
try {
|
||
return JSON.stringify(val)
|
||
} catch {
|
||
return ""
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
return walk(body.detail) || walk(body.message) || walk(body.msg)
|
||
}
|
||
|
||
/** 无响应体时按 HTTP 状态码给出兜底提示(与 client.ts 拦截器口径一致) */
|
||
function statusFallback(status: number): string {
|
||
switch (status) {
|
||
case 400:
|
||
return "请求参数有误(HTTP 400)"
|
||
case 401:
|
||
return "登录状态已失效,请重新登录(HTTP 401)"
|
||
case 403:
|
||
return "没有权限执行该操作(HTTP 403)"
|
||
case 404:
|
||
return "请求的资源不存在(HTTP 404)"
|
||
case 409:
|
||
return "操作冲突,资源状态已变化(HTTP 409)"
|
||
case 413:
|
||
return "文件过大,请缩小后重试(HTTP 413)"
|
||
case 415:
|
||
return "不支持的文件格式(HTTP 415)"
|
||
case 429:
|
||
return "操作过于频繁,请稍后再试(HTTP 429)"
|
||
case 503:
|
||
return "服务暂不可用,请稍后再试(HTTP 503)"
|
||
default:
|
||
if (status >= 500) return `服务器繁忙,请稍后再试(HTTP ${status})`
|
||
return `请求失败(HTTP ${status})`
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从任意抛出值提取可展示的错误信息。
|
||
* @param fallback 全部提取失败时的兜底文案
|
||
*/
|
||
export function getErrorMessage(err: unknown, fallback = "操作失败,请稍后重试"): string {
|
||
if (!err) return fallback
|
||
|
||
// axios 错误(后端 JSON 响应 / HTTP 错误状态)
|
||
const ax = err as AxiosError<ErrorBody>
|
||
if (ax.isAxiosError || (typeof ax === "object" && "response" in (ax as object))) {
|
||
// 超时
|
||
if (ax.code === "ECONNABORTED" || /timeout/i.test(ax.message || "")) {
|
||
return "请求超时,请检查网络后重试"
|
||
}
|
||
const resp = ax.response
|
||
if (resp) {
|
||
const bodyMsg = extractBodyMessage(resp.data)
|
||
if (bodyMsg) return bodyMsg
|
||
return statusFallback(resp.status)
|
||
}
|
||
// 请求已发出但无响应(断网/CORS/DNS)
|
||
if (ax.request) return "网络连接异常,请检查网络设置"
|
||
return ax.message || fallback
|
||
}
|
||
|
||
if (err instanceof Error) {
|
||
// XHR 直传 OSS 失败等场景自带详细 message(含 HTTP 状态 + OSS Code/Message)
|
||
if (err.message) return err.message
|
||
}
|
||
if (typeof err === "string") return err
|
||
|
||
return fallback
|
||
}
|
||
|
||
/** client.ts 拦截器是否已对该错误弹过全局 toast(__msgShown 标记) */
|
||
export function isErrorMsgShown(err: unknown): boolean {
|
||
return Boolean((err as { __msgShown?: boolean } | null)?.__msgShown)
|
||
}
|