import axios, { type AxiosRequestConfig } from "axios"; import { showErrorNative } from "../host/dialog"; import { getSession, setSession } from "../host/session"; import type { SessionState } from "../host/types"; export const API_QUEUE_CALLER_PATH = "/api/queue/caller"; const DEFAULT_API_PORT = 8845; const AUTH_LOGIN_PATH = "/auth/login"; const AUTH_REFRESH_PATH = "/auth/refresh"; type ApiEnvelope = { code: number; msg?: string; message?: string; data: T; }; type RefreshTokenResponse = { queueToken?: string; refreshToken?: string; }; type RetryableAxiosRequestConfig = AxiosRequestConfig & { _retry?: boolean; _skipAuthRefresh?: boolean; }; let refreshTokenPromise: Promise | null = null; /** * 根据配置中的服务器地址拼出后端 baseURL。 */ export function buildBaseUrlFromServerIp(serverIp: string): string { const raw = serverIp.trim(); if (!raw) { return ""; } if (raw.startsWith("http://") || raw.startsWith("https://")) { return `${raw.replace(/\/$/, "")}${API_QUEUE_CALLER_PATH}`; } const hostPort = raw.replace(/^\/+/, ""); const hasPort = /:\d+$/.test(hostPort) || /^\[.+\]:\d+$/.test(hostPort); if (hasPort) { return `http://${hostPort}${API_QUEUE_CALLER_PATH}`; } return `http://${hostPort}:${DEFAULT_API_PORT}${API_QUEUE_CALLER_PATH}`; } const instance = axios.create({ baseURL: "", timeout: 10000, headers: { "Content-Type": "application/json", }, }); function isApiSuccessCode(code: unknown): boolean { return code === 200 || code === 0; } function getApiMessage(payload: unknown): string { const data = (payload ?? {}) as { message?: unknown; msg?: unknown }; if (typeof data.message === "string" && data.message.trim() !== "") { return data.message; } if (typeof data.msg === "string" && data.msg.trim() !== "") { return data.msg; } return "请求失败"; } function isAuthEndpoint(url?: string): boolean { if (!url) { return false; } return url.includes(AUTH_LOGIN_PATH) || url.includes(AUTH_REFRESH_PATH); } function setAuthHeader(config: AxiosRequestConfig, queueToken: string): void { const headers = (config.headers ?? {}) as Record; headers.Authorization = `Bearer ${queueToken}`; config.headers = headers; } async function refreshQueueToken(): Promise { if (refreshTokenPromise) { return refreshTokenPromise; } refreshTokenPromise = (async () => { const sessionState = await getSession(); const refreshToken = sessionState.refreshToken; if (!refreshToken) { return null; } const response = await axios.request>({ baseURL: instance.defaults.baseURL, timeout: instance.defaults.timeout, method: "POST", url: AUTH_REFRESH_PATH, data: { refreshToken }, headers: { "Content-Type": "application/json" }, }); const payload = response.data; if (!isApiSuccessCode(payload?.code) || !payload?.data?.queueToken) { return null; } const refreshedQueueToken = payload.data.queueToken; await setSession({ ...sessionState, queueToken: refreshedQueueToken, refreshToken: payload.data.refreshToken ?? refreshToken, }); return refreshedQueueToken; })(); try { return await refreshTokenPromise; } finally { refreshTokenPromise = null; } } function canRefreshAndRetry(config?: RetryableAxiosRequestConfig): boolean { if (!config) { return false; } if (config._retry || config._skipAuthRefresh) { return false; } return !isAuthEndpoint(config.url); } async function retryWithRefreshedToken(config: RetryableAxiosRequestConfig): Promise { const nextToken = await refreshQueueToken(); if (!nextToken) { throw new Error("会话已过期,请重新登录"); } const retryConfig: RetryableAxiosRequestConfig = { ...config, _retry: true, }; setAuthHeader(retryConfig, nextToken); return instance.request(retryConfig); } /** * 将配置中的服务器地址应用到 axios 实例。 */ export function applyServerIpToHttp(serverIp: string): void { if (import.meta.env.DEV) { instance.defaults.baseURL = API_QUEUE_CALLER_PATH; return; } instance.defaults.baseURL = buildBaseUrlFromServerIp(serverIp); } instance.interceptors.request.use( async (config) => { if (!instance.defaults.baseURL) { throw new Error("未配置服务器地址,请先完成服务地址设置"); } let token: string | null = null; try { const sessionState: SessionState = await getSession(); token = sessionState.queueToken; } catch { token = null; } if (token && !isAuthEndpoint(config.url)) { setAuthHeader(config, token); } return config; }, async (error) => { throw error; }, ); instance.interceptors.response.use( async (response) => { const { data } = response; const payload = data as ApiEnvelope; if (!isApiSuccessCode(payload?.code)) { const requestConfig = response.config as RetryableAxiosRequestConfig; if (payload?.code === 401 && canRefreshAndRetry(requestConfig)) { return retryWithRefreshedToken(requestConfig); } const message = getApiMessage(payload); await showErrorNative(message); throw new Error(message); } return payload.data; }, async (error) => { if (error.response) { const requestConfig = error.config as RetryableAxiosRequestConfig; if (error.response.status === 401 && canRefreshAndRetry(requestConfig)) { try { return await retryWithRefreshedToken(requestConfig); } catch { await showErrorNative("未授权,请重新登录"); throw error; } } switch (error.response.status) { case 400: await showErrorNative("请求错误"); break; case 401: await showErrorNative("未授权,请重新登录"); break; case 403: await showErrorNative("拒绝访问"); break; case 404: await showErrorNative("请求地址不存在"); break; case 500: await showErrorNative("服务器内部错误"); break; case 502: await showErrorNative("网关错误"); break; case 503: await showErrorNative("服务不可用"); break; default: await showErrorNative("请求失败"); } } else if (error.request) { await showErrorNative("网络连接异常"); } else { await showErrorNative(error.message); } throw error; }, ); export const http = { /** * 发送原生 axios 请求。 */ request(config: AxiosRequestConfig): Promise { return instance.request(config); }, /** * 发送 GET 请求。 */ get(url: string, params?: unknown, config?: AxiosRequestConfig): Promise { return instance.get(url, { params, ...config }); }, /** * 发送 POST 请求。 */ post(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { return instance.post(url, data, config); }, /** * 发送 PUT 请求。 */ put(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { return instance.put(url, data, config); }, /** * 发送 DELETE 请求。 */ delete(url: string, params?: unknown, config?: AxiosRequestConfig): Promise { return instance.delete(url, { params, ...config }); }, /** * 发送 PATCH 请求。 */ patch(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { return instance.patch(url, data, config); }, };