import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios' import { ElMessage } from 'element-plus' /** * 用户操作分布:哪些模块路径算"业务操作"(用于看板统计) * 请求命中这些路径时,会自动调用 record_visit 记录一次(同一模块 60 秒内去重) */ const VISIT_TRACKED_PREFIXES: Array<[string, string]> = [ ['/fine-tune', 'fine-tune'], ['/model-eval', 'model-eval'], ['/model-inference', 'model-inference'], ['/model-compare', 'model-inference'], ['/data-process', 'data-process'], ['/data-convert', 'data-convert'], ['/model-manage', 'model-manage'], ['/dataset-manage', 'dataset'], ] function trackVisit(url: string | undefined) { if (!url) return for (const [prefix, module] of VISIT_TRACKED_PREFIXES) { if (url.includes(prefix)) { const key = `visit:${module}` const last = Number(sessionStorage.getItem(key) || 0) if (Date.now() - last < 60000) return // 60 秒内去重 sessionStorage.setItem(key, String(Date.now())) // fire-and-forget 调用后端记录接口 import('./modules/audit-visit').then(({ recordModuleVisit }) => { recordModuleVisit(module, url).catch(() => { /* ignore */ }) }).catch(() => { /* ignore */ }) return } } } /** * 后端统一响应格式 * code === 0 表示成功,data 为业务数据 */ export interface ApiResult { code: number message?: string data: T } const service: AxiosInstance = axios.create({ // Use a relative path; Vite proxies /modelTF to http://localhost:17861 in local development. baseURL: '/modelTF', timeout: 120000, }) /** * 从 localStorage 取当前用户 token(登录时后端返回 platform-token-{user_id})。 * 后端鉴权中间件依赖此 header 解析当前用户身份。 */ function getAuthToken(): string | null { const USER_STORAGE_KEY = 'currentUser' const raw = localStorage.getItem(USER_STORAGE_KEY) if (raw) { try { const user = JSON.parse(raw) // 后端 login 返回的 token 格式为 platform-token-{user.id} if (user?.id) return `platform-token-${user.id}` } catch { /* ignore */ } } // 兼容改造前 admin 会话 if (localStorage.getItem('username') === 'admin') return 'platform-token-admin' return null } function getActiveTenantId(): string | null { return localStorage.getItem('activeTenantId') } /** Headers shared by Axios and raw fetch requests such as SSE streaming. */ export function getAuthHeaders(): Record { const headers: Record = {} const token = getAuthToken() if (token) headers.Authorization = `Bearer ${token}` const tenantId = getActiveTenantId() if (tenantId) headers['X-Tenant-ID'] = tenantId return headers } // 请求拦截器:注入 Authorization header service.interceptors.request.use( (config) => { const authHeaders = getAuthHeaders() if (Object.keys(authHeaders).length) { config.headers = config.headers || {} Object.assign(config.headers, authHeaders) } return config }, (error) => Promise.reject(error), ) // 响应拦截器:统一解包 { code, data, message } service.interceptors.response.use( (response) => { const res = response.data as ApiResult // 二进制流等非 JSON 响应直接返回 if (response.config.responseType === 'blob' || response.config.responseType === 'arraybuffer') { return response } if (res.code === 0) { return res.data } // 业务错误 const message = res.message || '请求失败' ElMessage.error(message) return Promise.reject(new Error(message)) }, (error) => { const detail = error.response?.data?.detail const message = detail?.message || error.response?.data?.message || detail || error.message || '网络异常' ElMessage.error(message) return Promise.reject(error) }, ) /** GET 请求,返回已解包的 data */ export function get(url: string, params?: any, config?: AxiosRequestConfig): Promise { return service.get(url, { params, ...config }) as unknown as Promise } /** POST 请求 */ export function post(url: string, data?: any, config?: AxiosRequestConfig): Promise { return service.post(url, data, config) as unknown as Promise } /** PUT 请求 */ export function put(url: string, data?: any, config?: AxiosRequestConfig): Promise { return service.put(url, data, config) as unknown as Promise } /** DELETE 请求 */ export function del(url: string, params?: any, config?: AxiosRequestConfig): Promise { return service.delete(url, { params, ...config }) as unknown as Promise } export default service