import { ajax, isNativeApp } from '@/core/mxApi' import type { MXAjaxType } from '@/core/mxApi' import { request as axiosRequest } from '@/utils/request' import type { RequestConfig } from '@/utils/request' import { appendQuery } from '@/utils/query' import { apiConfig } from '@/config/env' import type { ApiResponse } from '@/types/api' import { getAuthHeaders, getStoredRefreshToken, REFRESH_TOKEN_URL, refreshAccessToken, type AuthTokenResponse, } from '@/utils/auth-token' import { handleAuthFailure } from '@/utils/auth-failure' export type { RequestConfig } from '@/utils/request' export function request( config: RequestConfig, ): Promise> { if (isNativeApp()) { return nativeRequest(config) } return axiosRequest(config) } async function nativeRequest( config: RequestConfig, retried = false, ): Promise> { const method = (config.method || 'GET').toUpperCase() if (!isMXAjaxType(method)) { throw new Error(`敏行 AJAX 不支持 ${method} 请求`) } try { const response = await ajax>({ type: method, url: appendQuery(resolveNativeUrl(config.url), config.params), data: config.data, dataType: 'text', async: true, headers: getCommonHeaders(config.headers, config.skipAuth), }) // 敏行宿主会把 TAM 的业务失败作为 AJAX success 返回,需在此识别认证码。 if (isUnauthorized(response.data)) { throw response.data } return response.data } catch (error) { if (!isUnauthorized(error) || config.skipAuth) { throw error } // 新 token 重试后仍认证失败,说明登录态已无法恢复,清理并提示重新登录。 if (retried) { void handleAuthFailure() throw error } // 本地没有 refresh token 时无法刷新,直接清理登录态并提示重新登录。 if (!getStoredRefreshToken()) { void handleAuthFailure() throw error } try { await refreshAccessToken(requestRefreshTokenByNative) } catch (refreshError) { void handleAuthFailure() throw refreshError } return nativeRequest(config, true) } } function getCommonHeaders(headers?: Record, skipAuth = false) { return getAuthHeaders(headers, skipAuth) } function isUnauthorized(value: unknown) { if (isUnauthorizedCode(value)) return true if (!isRecord(value)) return false if (hasUnauthorizedCode(value)) return true const response = isRecord(value.response) ? value.response : undefined if (hasUnauthorizedCode(response)) return true const responseData = response && isRecord(response.data) ? response.data : undefined if (hasUnauthorizedCode(responseData)) return true // 兼容宿主或调用方额外包裹一层 data 的 TAM 响应,避免扫描普通业务 data.code。 const data = isRecord(value.data) ? value.data : undefined return hasUnauthorizedHeadCode(data) } function hasUnauthorizedCode(value?: Record) { if (!value) return false return ( isUnauthorizedCode(value.status) || isUnauthorizedCode(value.statusCode) || isUnauthorizedCode(value.code) || isUnauthorizedCode(value.retCode) || hasUnauthorizedHeadCode(value) ) } function hasUnauthorizedHeadCode(value?: Record) { const head = value && isRecord(value.head) ? value.head : undefined return isUnauthorizedCode(head?.retCode) } function isUnauthorizedCode(code: unknown) { return code === 401 || code === '401' || code === '0401' } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null } async function requestRefreshTokenByNative() { const refreshToken = getStoredRefreshToken() if (!refreshToken) { throw new Error('本地不存在 refresh token') } const response = await ajax & AuthTokenResponse>({ type: 'POST', url: resolveNativeUrl(REFRESH_TOKEN_URL), data: { refresh_token: refreshToken, }, dataType: 'text', async: true, headers: getCommonHeaders(undefined, true), }) if (typeof response.data.code === 'number' && response.data.code !== 0) { throw new Error(response.data.message || '刷新 token 失败') } return response.data.data || response.data } function isMXAjaxType(method: string): method is MXAjaxType { return method === 'GET' || method === 'POST' || method === 'PUT' || method === 'DELETE' } function resolveNativeUrl(url: string) { if (/^[a-z][a-z\d+\-.]*:\/\//i.test(url)) return url const baseURL = apiConfig.baseURL || '' if (!baseURL) return url return `${baseURL.replace(/\/+$/, '')}/${url.replace(/^\/+/, '')}` }