2026-04-28 11:33:17 +08:00
|
|
|
import axios, { type AxiosError, type AxiosInstance } from "axios";
|
|
|
|
|
|
|
|
|
|
import { clearStoredSession, readStoredSession } from "@/lib/storage";
|
|
|
|
|
|
|
|
|
|
export class OpsApiError extends Error {
|
|
|
|
|
status?: number;
|
|
|
|
|
code: number;
|
|
|
|
|
detail?: string;
|
|
|
|
|
requestId?: string;
|
|
|
|
|
|
|
|
|
|
constructor(input: {
|
|
|
|
|
message: string;
|
|
|
|
|
code?: number;
|
|
|
|
|
status?: number;
|
|
|
|
|
detail?: string;
|
|
|
|
|
requestId?: string;
|
|
|
|
|
}) {
|
|
|
|
|
super(input.message);
|
|
|
|
|
this.name = "OpsApiError";
|
|
|
|
|
this.code = input.code ?? 50000;
|
|
|
|
|
this.status = input.status;
|
|
|
|
|
this.detail = input.detail;
|
|
|
|
|
this.requestId = input.requestId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ApiEnvelope<T> {
|
|
|
|
|
code: number;
|
|
|
|
|
message: string;
|
|
|
|
|
detail?: string;
|
|
|
|
|
data: T;
|
|
|
|
|
request_id?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let onUnauthorized: (() => void) | null = null;
|
|
|
|
|
|
|
|
|
|
export function bindUnauthorizedHandler(handler: () => void): void {
|
|
|
|
|
onUnauthorized = handler;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const client: AxiosInstance = axios.create({
|
|
|
|
|
baseURL: "/api/ops",
|
|
|
|
|
timeout: 30000,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
client.interceptors.request.use((config) => {
|
2026-04-29 15:59:23 +08:00
|
|
|
const { session } = readStoredSession();
|
2026-04-28 11:33:17 +08:00
|
|
|
if (session.accessToken) {
|
|
|
|
|
config.headers = config.headers ?? {};
|
|
|
|
|
config.headers["Authorization"] = `Bearer ${session.accessToken}`;
|
|
|
|
|
}
|
|
|
|
|
return config;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
client.interceptors.response.use(
|
|
|
|
|
(response) => response,
|
|
|
|
|
(error: AxiosError<ApiEnvelope<unknown>>) => {
|
|
|
|
|
const status = error.response?.status;
|
|
|
|
|
const payload = error.response?.data;
|
|
|
|
|
|
|
|
|
|
if (status === 401) {
|
|
|
|
|
clearStoredSession();
|
|
|
|
|
onUnauthorized?.();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Promise.reject(
|
|
|
|
|
new OpsApiError({
|
|
|
|
|
message: payload?.message ?? error.message ?? "request_failed",
|
|
|
|
|
code: typeof payload?.code === "number" ? payload.code : undefined,
|
|
|
|
|
status,
|
|
|
|
|
detail: typeof payload?.detail === "string" ? payload.detail : undefined,
|
|
|
|
|
requestId: typeof payload?.request_id === "string" ? payload.request_id : undefined,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
async function unwrap<T>(promise: Promise<{ data: ApiEnvelope<T> }>): Promise<T> {
|
|
|
|
|
const { data } = await promise;
|
|
|
|
|
if (data.code !== 0) {
|
|
|
|
|
throw new OpsApiError({
|
|
|
|
|
message: data.message,
|
|
|
|
|
code: data.code,
|
|
|
|
|
detail: data.detail,
|
|
|
|
|
requestId: data.request_id,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return data.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const http = {
|
|
|
|
|
get: <T>(url: string, params?: Record<string, unknown>) =>
|
|
|
|
|
unwrap<T>(client.get(url, { params })),
|
|
|
|
|
post: <T, B = unknown>(url: string, body?: B) => unwrap<T>(client.post(url, body)),
|
|
|
|
|
patch: <T, B = unknown>(url: string, body?: B) => unwrap<T>(client.patch(url, body)),
|
|
|
|
|
delete: <T>(url: string) => unwrap<T>(client.delete(url)),
|
|
|
|
|
};
|