You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

301 lines
7.7 KiB
TypeScript

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<T = unknown> = {
code: number;
msg?: string;
message?: string;
data: T;
};
type RefreshTokenResponse = {
queueToken?: string;
refreshToken?: string;
};
type RetryableAxiosRequestConfig = AxiosRequestConfig & {
_retry?: boolean;
_skipAuthRefresh?: boolean;
};
let refreshTokenPromise: Promise<string | null> | 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<string, string>;
headers.Authorization = `Bearer ${queueToken}`;
config.headers = headers;
}
async function refreshQueueToken(): Promise<string | null> {
if (refreshTokenPromise) {
return refreshTokenPromise;
}
refreshTokenPromise = (async () => {
const sessionState = await getSession();
const refreshToken = sessionState.refreshToken;
if (!refreshToken) {
return null;
}
const response = await axios.request<ApiEnvelope<RefreshTokenResponse>>({
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<any> {
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<T = unknown>(config: AxiosRequestConfig): Promise<T> {
return instance.request(config);
},
/**
* 发送 GET 请求。
*/
get<T = unknown>(url: string, params?: unknown, config?: AxiosRequestConfig): Promise<T> {
return instance.get(url, { params, ...config });
},
/**
* 发送 POST 请求。
*/
post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
return instance.post(url, data, config);
},
/**
* 发送 PUT 请求。
*/
put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
return instance.put(url, data, config);
},
/**
* 发送 DELETE 请求。
*/
delete<T = unknown>(url: string, params?: unknown, config?: AxiosRequestConfig): Promise<T> {
return instance.delete(url, { params, ...config });
},
/**
* 发送 PATCH 请求。
*/
patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
return instance.patch(url, data, config);
},
};