import { router } from 'expo-router'; import Toast from '@/components/ui/Toast'; import { storage } from './storage'; // 统一的 Token 存储键名(与 lib/auth.ts 保持一致) export const TOKEN_KEY = 'bestaibest.better-auth.session_token'; interface FetchLoggerOptions { enableLogging?: boolean logRequest?: boolean logResponse?: boolean logError?: boolean } const defaultOptions: FetchLoggerOptions = { enableLogging: __DEV__, logRequest: true, logResponse: true, logError: true, } const originalFetch = global.fetch // 防止重复跳转的标志 let isRedirecting = false export const createFetchWithLogger = (options: FetchLoggerOptions = {}) => { const config = { ...defaultOptions, ...options } return async (input: RequestInfo | URL, init?: RequestInit): Promise => { const startTime = Date.now() const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url const method = init?.method || 'GET' try { // 使用统一的 Token 存储键 const token = await storage.getItem(TOKEN_KEY) // 确保 headers 是一个对象 const headers: Record = {} if (init?.headers) { if (init.headers instanceof Headers) { init.headers.forEach((value, key) => { headers[key] = value }) } else if (Array.isArray(init.headers)) { init.headers.forEach(([key, value]) => { headers[key] = value as string }) } else { Object.assign(headers, init.headers) } } // 添加 Bearer Token if (token) { headers['authorization'] = `Bearer ${token}` } // 确保 Content-Type 正确设置(FormData 不需要手动设置,让浏览器自动处理 boundary) if (method !== 'GET' && method !== 'HEAD' && !headers['Content-Type'] && !(init?.body instanceof FormData)) { headers['Content-Type'] = 'application/json' } // 序列化请求体(如果是对象) let body = init?.body if (body && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof URLSearchParams)) { body = JSON.stringify(body) } const response = await originalFetch(input, { ...init, headers, ...(body !== undefined && { body }), }) // 401 未授权处理 if (response.status === 401 && !isRedirecting) { // 检查是否是登录/注册接口的 401 错误(用户名密码错误) const isAuthEndpoint = url.includes('/sign-in/') || url.includes('/sign-up/') // 如果是认证接口的 401,不处理,让错误传递到组件 if (isAuthEndpoint) { console.log('🔐 认证接口返回 401,不处理') return response } // 其他接口的 401 才认为是 Token 过期 console.warn('🔐 401 未授权,跳转到登录页') isRedirecting = true // 显示 Toast 提示(安全检查) try { Toast.show('登录已过期,请重新登录') } catch (error) { console.warn('[Toast] 显示失败:', error) } // 清除过期 Token await storage.removeItem(TOKEN_KEY) // 跳转到登录页(使用 replace 避免返回栈问题) router.replace('/auth') // 延迟重置标志(防止并发请求导致重复跳转) setTimeout(() => { isRedirecting = false }, 1000) } return response } catch (error) { const duration = Date.now() - startTime if (config.enableLogging && config.logError) { console.group(`❌ [FETCH 错误] ${method} ${url}`) console.error('📍 URL:', url) console.error('🔧 Method:', method) console.error('⏱️ Duration:', `${duration}ms`) if (init?.headers) { console.error('📋 Request Headers:', JSON.stringify(init.headers, null, 2)) } if (init?.body) { try { const bodyContent = typeof init.body === 'string' ? init.body : JSON.stringify(init.body) console.error('📦 Request Body:', bodyContent) } catch (e) { console.error('📦 Request Body: [无法序列化]') } } console.error('💥 Error Type:', error?.constructor?.name || 'Unknown') console.error('💥 Error Message:', error instanceof Error ? error.message : String(error)) if (error instanceof Error && error.stack) { console.error('📚 Stack Trace:', error.stack) } if (error && typeof error === 'object') { console.error('🔍 Error Details:', JSON.stringify(error, Object.getOwnPropertyNames(error), 2)) } console.groupEnd() } throw error } } } export const fetchWithLogger = createFetchWithLogger() export const setupGlobalFetchLogger = (options?: FetchLoggerOptions) => { const loggedFetch = createFetchWithLogger(options) global.fetch = loggedFetch as typeof fetch return () => { global.fetch = originalFetch } }