import type { AuthResponse } from '@/types/api' const DEFAULT_API_URL = 'http://localhost:8000/api/v1' export const API_URL = (import.meta.env.VITE_API_URL || DEFAULT_API_URL).replace(/\/$/, '') export const AUTH_UPDATED_EVENT = 'opencloud:auth-updated' export const AUTH_EXPIRED_EVENT = 'opencloud:auth-expired' let accessToken: string | null = null let refreshPromise: Promise | null = null export class ApiError extends Error { status: number detail: unknown constructor(message: string, status: number, detail?: unknown) { super(message) this.name = 'ApiError' this.status = status this.detail = detail } } interface ApiRequestOptions extends Omit { body?: BodyInit | Record | null auth?: boolean retry?: boolean } function isJsonBody(body: ApiRequestOptions['body']): body is Record { return !!body && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof Blob) && !(body instanceof URLSearchParams) && !(body instanceof ArrayBuffer) } function detailToMessage(detail: unknown, fallback: string) { if (typeof detail === 'string') return detail if (Array.isArray(detail)) { const messages = detail .map(item => item && typeof item === 'object' && 'msg' in item ? String(item.msg) : '') .filter(Boolean) if (messages.length) return messages.join(';') } return fallback } async function toApiError(response: Response) { let detail: unknown try { const payload = await response.json() as { detail?: unknown } detail = payload.detail } catch { detail = undefined } return new ApiError( detailToMessage(detail, `请求失败(${response.status})`), response.status, detail, ) } export function setAccessToken(token: string | null) { accessToken = token } export function clearAccessToken() { accessToken = null } async function request(path: string, options: ApiRequestOptions = {}) { const { body, auth = true, retry = true, ...requestOptions } = options const headers = new Headers(requestOptions.headers) let requestBody: BodyInit | null | undefined = body as BodyInit | null | undefined if (isJsonBody(body)) { headers.set('Content-Type', 'application/json') requestBody = JSON.stringify(body) } if (auth && accessToken) { headers.set('Authorization', `Bearer ${accessToken}`) } const response = await fetch(`${API_URL}${path}`, { ...requestOptions, body: requestBody, headers, credentials: 'include', }) if (response.status === 401 && auth && accessToken && retry) { try { const authResponse = await refreshSession() window.dispatchEvent(new CustomEvent(AUTH_UPDATED_EVENT, { detail: authResponse })) return request(path, { ...options, retry: false }) } catch { clearAccessToken() window.dispatchEvent(new Event(AUTH_EXPIRED_EVENT)) } } if (!response.ok) throw await toApiError(response) if (response.status === 204) return undefined as T return response.json() as Promise } export function apiRequest(path: string, options: ApiRequestOptions = {}) { return request(path, options) } export function refreshSession() { if (!refreshPromise) { refreshPromise = (async () => { const response = await fetch(`${API_URL}/auth/refresh`, { method: 'POST', credentials: 'include', }) if (!response.ok) throw await toApiError(response) const authResponse = await response.json() as AuthResponse setAccessToken(authResponse.access_token) return authResponse })().finally(() => { refreshPromise = null }) } return refreshPromise }