import type { AxiosRequestConfig, AxiosResponse } from 'axios' import { locale, t as translate } from '@@/i18n/runtime' import { reportError } from '@@/utils/notifications' import axios, { AxiosHeaders, CanceledError, isAxiosError, isCancel } from 'axios' import { pinia } from '@/pinia' import { useUserStore } from '@/pinia/stores/user' export class ApiError extends Error { constructor(readonly status: number, message: string, readonly data: Record = {}, readonly code = status * 100) { super(message) } } interface RequestConfig extends Omit, 'auth'> { anonymous?: boolean errorOwner?: 'request' | 'caller' /** 原文和下载需要 ETag、Content-Disposition 等响应头。 */ returnResponse?: boolean } const instance = axios.create({ baseURL: '/api/v1', adapter: 'fetch', withCredentials: false }) function envelope(value: unknown, status: number): ApiResponseData { if (!value || typeof value !== 'object' || !('code' in value) || typeof value.code !== 'number' || !Number.isInteger(value.code) || !('data' in value) || !('message' in value) || typeof value.message !== 'string') { throw new ApiError(status, translate('common.invalid_server_response_protocol_194dc7')) } return value as ApiResponseData } function errorData(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {} } async function failure(error: unknown): Promise { if (!isAxiosError(error) || !error.response) return error instanceof ApiError ? error : new ApiError(0, error instanceof Error ? error.message : translate('common.request_failed_126fbf')) const response = error.response let body: unknown = response.data if (body instanceof Blob) body = await body.text() if (typeof body === 'string') { try { body = JSON.parse(body) } catch { return new ApiError(response.status, translate('common.request_failed_http_f21a09', { p0: response.status })) } } try { const data = envelope(body, response.status) return new ApiError(response.status, data.message, errorData(data.data), data.code) } catch (cause) { if (cause instanceof ApiError) return cause throw cause } } export function request(config: RequestConfig & { returnResponse: true }): Promise> export function request(config: RequestConfig): Promise export async function request(config: RequestConfig): Promise> { const user = useUserStore(pinia) const version = user.getSessionVersion() const token = config.anonymous === true ? '' : user.token const headers = new AxiosHeaders(config.headers as ConstructorParameters[0]) headers.set('Accept-Language', locale.value) headers.set('Authorization', token ? `Bearer ${token}` : undefined) try { const response = await instance.request>({ ...config, headers, signal: config.signal ? AbortSignal.any([user.getSessionSignal(), config.signal as AbortSignal]) : user.getSessionSignal(), fetchOptions: { ...config.fetchOptions, credentials: 'omit', cache: 'no-store' }, }) user.assertSession(version) if (response.status === 204) return config.returnResponse ? response : undefined as T if (!config.responseType || config.responseType === 'json') { const result = envelope(response.data, response.status) if (result.code !== 0) throw new ApiError(response.status, result.message, errorData(result.data), result.code) } return config.returnResponse ? response : response.data } catch (cause) { if (isCancel(cause)) throw cause if (version !== user.getSessionVersion()) throw new CanceledError(translate('common.session_changed_073f28')) const error = await failure(cause) user.assertSession(version) if (error.status === 401 && token) user.invalidate(version) if (config.errorOwner !== 'caller') reportError(error) throw error } }