import type { FetchContext } from 'ofetch' import type { ApiResponse } from '~/types/api' import CryptoJS from 'crypto-js' import { v4 as uuidv4 } from 'uuid' import { APP_LOCALE_KEY, DEVICE_UDID_KEY } from '~/constants' import { showToast } from 'vant' import { getDeviceLanguage } from '~/utils/helpers' const MD5 = CryptoJS.MD5 // Device and app information (fallback values) const DEVICE_INFO = { udid: '', // Will be initialized app: 'web', device: '', platform: 3, channel: 'official', api: 1, version: '1.0.0', network: 'wifi', } /** * Initialize device info (fallback for when bridge headers are missing) */ const initDeviceInfo = () => { if (import.meta.client) { let udid = localStorage.getItem(DEVICE_UDID_KEY) if (!udid) { udid = uuidv4() localStorage.setItem(DEVICE_UDID_KEY, udid) } DEVICE_INFO.udid = udid DEVICE_INFO.device = (navigator.userAgent || 'unknown').slice(0, 256) interface NetworkInformation { effectiveType?: string } const nav = navigator as Navigator & { connection?: NetworkInformation mozConnection?: NetworkInformation webkitConnection?: NetworkInformation } const connection = nav.connection || nav.mozConnection || nav.webkitConnection if (connection) { DEVICE_INFO.network = connection.effectiveType || 'wifi' } } } /** * Generate request ID */ const generateRequestId = (): string => { return uuidv4().replace(/-/g, '') } type SignDeviceInfo = { udid: string app: string device: string platform: string | number channel: string api: string | number version: string network: string } /** * Generate sign (same algorithm as web request.ts) */ const generateSign = (id: string, time: number, token: string, secret: string, body: unknown, deviceInfo: SignDeviceInfo): string => { const { udid, app, device, platform, channel, api, version, network } = deviceInfo const bodyStr = body ? JSON.stringify(body) : '' const signStr = `${id}${udid}${app}${device}${platform}${channel}${api}${version}${network}${time}${token}${secret}${bodyStr}` return MD5(signStr).toString() } /** * Create request instance for native pages. * Related headers are read from window.GAMI_BRIDGE.requestHeader, while `id` and `sign` are generated locally. */ export const createRequestNative = () => { const runtimeConfig = useRuntimeConfig() const { token, logout } = useAuth() const nuxtApp = useNuxtApp() // eslint-disable-next-line @typescript-eslint/no-explicit-any const t = ((nuxtApp as any)?.$i18n?.t?.bind((nuxtApp as any)?.$i18n) as ((key: string) => string) | undefined) || ((key: string) => key) const API_CONFIG = { baseURL: runtimeConfig.public.apiBase || '/api', timeout: 30000, secret: 'abc|abc|edg|9527|1234', identity: runtimeConfig.public.apiIdentity || 'web', } if (import.meta.client && !DEVICE_INFO.udid) { initDeviceInfo() } const getBridgeHeaders = (): Record => { if (!import.meta.client) return {} const raw = window.GAMI_BRIDGE?.requestHeader if (!raw || typeof raw !== 'object') { console.error('Must in native context') showError(t('common.errors.nativeOnly')) return {} } const out: Record = {} for (const [k, v] of Object.entries(raw)) { if (v === undefined || v === null) continue out[k] = String(v) } return out } const onRequest = (ctx: FetchContext): void => { const { options } = ctx if (!options.baseURL) { options.baseURL = API_CONFIG.baseURL } const headers: Record = {} if (options.headers) { const existingHeaders = options.headers as unknown as Record Object.assign(headers, existingHeaders) } const bridgeHeaders = getBridgeHeaders() Object.assign(headers, bridgeHeaders) const requestId = generateRequestId() const time = Date.now() const tokenVal = bridgeHeaders.token || token.value || '' if (import.meta.client) { const language = localStorage.getItem(APP_LOCALE_KEY) headers['Accept-Language'] = language || getDeviceLanguage() } const signDeviceInfo: SignDeviceInfo = { udid: bridgeHeaders.udid || DEVICE_INFO.udid, app: bridgeHeaders.app || API_CONFIG.identity, device: bridgeHeaders.device || DEVICE_INFO.device, platform: bridgeHeaders.platform ?? DEVICE_INFO.platform, channel: bridgeHeaders.channel || DEVICE_INFO.channel, api: bridgeHeaders.api ?? DEVICE_INFO.api, version: bridgeHeaders.version || DEVICE_INFO.version, network: bridgeHeaders.network || DEVICE_INFO.network, } // Ensure mandatory API headers exist (from bridge or fallback), but override id/time/sign headers['id'] = requestId headers['udid'] = String(signDeviceInfo.udid) headers['app'] = String(signDeviceInfo.app) headers['device'] = String(signDeviceInfo.device) headers['platform'] = String(signDeviceInfo.platform) headers['channel'] = String(signDeviceInfo.channel) headers['api'] = String(signDeviceInfo.api) headers['version'] = String(signDeviceInfo.version) headers['network'] = String(signDeviceInfo.network) headers['time'] = String(time) if (tokenVal) headers['token'] = tokenVal else delete headers.token const body = options.body headers['sign'] = generateSign(requestId, time, tokenVal, API_CONFIG.secret, body, signDeviceInfo) if (!headers['Content-Type']) { headers['Content-Type'] = 'application/json' } // @ts-expect-error - Headers type mismatch between HeadersInit and Headers options.headers = headers } const onResponse = (ctx: FetchContext): void => { if (!ctx.response) return const data = ctx.response._data as ApiResponse if (data && typeof data === 'object' && 'code' in data) { if (data.code === 0) { ctx.response._data = data.data return } handleApiError(data.code, data.msg) throw new Error(data.msg || t('common.errors.requestFailed')) } } const onResponseError = (ctx: FetchContext): Promise => { const { error } = ctx const response = ctx.response if (response) { const status = response.status const data = response._data as { msg?: string } switch (status) { case 401: handleUnauthorized() break case 403: showError(t('common.errors.noPermission')) break case 404: showError(t('common.errors.notFound')) break case 500: showError(t('common.errors.serverError')) break default: showError(data?.msg || error?.message || t('common.errors.requestFailed')) } } else { const message = error?.message || '' if (message.includes('timeout')) showError(t('common.errors.timeout')) else if (message.includes('Network Error')) showError(t('common.errors.network')) else showError(message || t('common.errors.unknownError')) } return Promise.reject(error) } function handleApiError(code: number, message: string) { switch (code) { case 100004: handleUnauthorized() break default: if (message) showError(message) } } function handleUnauthorized() { logout() if (import.meta.client) { const route = useRoute() const redirect = route.fullPath showError(t('common.errors.loginExpired')) navigateTo({ path: '/login', query: redirect && redirect !== '/login' ? { redirect } : undefined, }) } } function showError(message: string) { if (import.meta.client) { showToast({ message: `${message}` }) } } const request = $fetch.create({ baseURL: API_CONFIG.baseURL, timeout: API_CONFIG.timeout, onRequest, onResponse, onResponseError, }) return request }