import axios, { AxiosError, type AxiosInstance, type InternalAxiosRequestConfig } from 'axios' const baseURL = (import.meta.env.VITE_API_BASE as string | undefined) ?? '/api' // AR-5 (会话 45):withCredentials=true 让后端下发的 httpOnly Cookie (simple.auth.token) // 自动随请求带回。Platform.Server `/api/auth/login` 同时下发 Cookie 和返回 token 字段, // 过渡期内 Authorization: Bearer header 仍然兼容(旧浏览器 / 跨进程脚本)。 const http: AxiosInstance = axios.create({ baseURL, timeout: 15000, headers: { 'Content-Type': 'application/json' }, withCredentials: true }) http.interceptors.request.use((config: InternalAxiosRequestConfig) => { // FormData 必须由浏览器自动带 multipart boundary;清掉默认 application/json。 if (typeof FormData !== 'undefined' && config.data instanceof FormData) { config.headers.delete('Content-Type') } // 双轨:优先用 localStorage 里的 token(过渡期 fallback),Cookie 会自动带; // 后端首先看 Authorization: Bearer,没有再看 Cookie,两路任一通过即可。 const token = localStorage.getItem('simple.auth.token') if (token) { config.headers.set('Authorization', `Bearer ${token}`) } const scope = localStorage.getItem('simple.auth.scope') if (scope) { config.headers.set('X-Scope', scope) } return config }) /** * 把 axios 异常翻译成更友好的中文文案,特别是 Simple3 链路: * * /api/sl/projection/** → Platform.Server YARP → Simple3 EmbedIO :8222 * * 如果 Simple3 没启动 / 端口未监听,YARP 一定回 502;超时一般是 504。把这两种 * 情况单独识别,写成「Simple3 未连接,请先启动 Simple3 (端口 8222)」之类, * 比直接抛 `Request failed with status code 502` 友好得多,也避免用户怀疑前端 bug。 */ function describeError(err: AxiosError): string { const status = err.response?.status const url = err.config?.url ?? '' const isSimple3 = url.startsWith('/sl/') || url.startsWith('sl/') if (status === 502 || status === 504 || err.code === 'ECONNABORTED' || err.code === 'ERR_NETWORK') { if (isSimple3) { return 'Simple3 未连接:请先启动 Simple3 后端(监听端口 8222)后重试。' } return `网关无法连接到下游服务(${status ?? err.code ?? 'network'})。` } // 后端 envelope 模式:{ success: false, message: 'xxx' } const data = err.response?.data as { message?: string } | undefined if (data?.message) return data.message return err.message } http.interceptors.response.use( (resp) => resp, (err: AxiosError) => { if (err.response?.status === 401) { // 会话 45 HC-1:401 全量清 state,避免老 user/scope/perm 残留导致 UI 误判。 try { localStorage.removeItem('simple.auth.token') localStorage.removeItem('simple.auth.user') localStorage.removeItem('simple.auth.scope') localStorage.removeItem('simple.auth.runMode') localStorage.removeItem('simple.auth.perm') } catch { /* 某些隐私模式 / iframe 沙箱可能禁用 localStorage */ } if (location.pathname !== '/login') location.assign('/login') } // 用 Object.defineProperty 覆盖 message,避免后续 `(err as Error).message` 还是看到原文。 const friendly = describeError(err) try { Object.defineProperty(err, 'message', { value: friendly, configurable: true }) } catch { /* ignore */ } return Promise.reject(err) } ) export default http