import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from "axios"; import { ElMessage } from "element-plus"; import { getToken } from "../utils/auth"; import type { ApiError } from "../types/api"; import { TEXT } from "../locales"; import { clientRuntime, DESKTOP_SERVER_URL_CHANGED_EVENT } from "../runtime"; const instance: AxiosInstance = axios.create({ baseURL: clientRuntime.apiBaseUrl(), timeout: 15000, }); export const refreshApiBaseUrl = (): void => { instance.defaults.baseURL = clientRuntime.apiBaseUrl(); }; if (typeof window !== "undefined") { window.addEventListener(DESKTOP_SERVER_URL_CHANGED_EVENT, refreshApiBaseUrl); } const NETWORK_RETRY_LIMIT = 10; const NETWORK_RETRY_DELAY_MS = 30000; export type ApiRequestConfig = AxiosRequestConfig & { suppressErrorMessage?: boolean; _retry?: boolean; _networkRetryCount?: number; }; const wait = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms)); const clearInvalidStudyAndNavigate = async () => { try { const [{ useStudyStore }, { default: router }] = await Promise.all([ import("../store/study"), import("../router"), ]); useStudyStore().clearCurrentStudy(); router.push("/admin/projects"); } catch { /* ignore */ } }; const forceAuthExpiredLogout = async () => { const { forceLogout, LOGOUT_REASON_AUTH_EXPIRED } = await import("../session/sessionManager"); forceLogout(LOGOUT_REASON_AUTH_EXPIRED); }; instance.interceptors.request.use((config: InternalAxiosRequestConfig & ApiRequestConfig) => { const token = getToken(); if (token) { config.headers = config.headers || {}; (config.headers as Record).Authorization = `Bearer ${token}`; } return config; }); instance.interceptors.response.use( (response: AxiosResponse) => response, async (error: AxiosError) => { const status = error.response?.status; const data = error.response?.data; const reqUrl = error.config?.url || ""; const isAuthEndpoint = reqUrl.includes("/api/v1/auth/login") || reqUrl.includes("/api/v1/auth/register") || reqUrl.includes("/api/v1/auth/password-reset") || reqUrl.includes("/api/v1/auth/extend"); if (isAuthEndpoint) { // 认证相关的错误由具体页面自行处理,避免重复提示 return Promise.reject(error); } if (!status && error.config) { const config = error.config as ApiRequestConfig; const retryCount = config._networkRetryCount || 0; if (retryCount < NETWORK_RETRY_LIMIT) { config._networkRetryCount = retryCount + 1; await wait(NETWORK_RETRY_DELAY_MS); return instance(config); } } // 如果当前项目不存在或已失效,清理项目并跳到项目管理页 if (status === 404 && typeof data?.message === "string" && data.message.toLowerCase().includes("study")) { await clearInvalidStudyAndNavigate(); } if (status === 401) { const config = error.config as ApiRequestConfig; if (config && !config._retry) { const { extendAccessToken } = await import("../session/sessionManager"); const result = await extendAccessToken("response-401"); if (result.token) { config._retry = true; config.headers = config.headers || {}; config.headers.Authorization = `Bearer ${result.token}`; return instance(config); } if (result.authFailed) { await forceAuthExpiredLogout(); } } } else if (status === 403 && (data as any)?.detail === "账号已停用") { await forceAuthExpiredLogout(); } else { const suppressErrorMessage = (error.config as ApiRequestConfig | undefined)?.suppressErrorMessage; if (!suppressErrorMessage) { if (data?.message || (data as any)?.detail) { const detail = (data as any)?.detail; const detailMessage = typeof detail === "string" ? detail : typeof detail?.message === "string" ? detail.message : undefined; ElMessage.error((data as any)?.message || detailMessage || TEXT.common.messages.requestFailed); } else { ElMessage.error(TEXT.common.messages.requestFailed); } } } return Promise.reject(error); } ); export const apiGet = (url: string, config?: ApiRequestConfig) => instance.get(url, config); export const apiPost = (url: string, data?: unknown, config?: ApiRequestConfig) => instance.post(url, data, config); export const apiPatch = (url: string, data?: unknown, config?: ApiRequestConfig) => instance.patch(url, data, config); export const apiPut = (url: string, data?: unknown, config?: ApiRequestConfig) => instance.put(url, data, config); export const apiDelete = (url: string, config?: ApiRequestConfig) => instance.delete(url, config); export default instance;