// 统一的 fetch 包装器:自动携带 Token、处理刷新和滑动过期 let tokenRefreshPromise: Promise | null = null async function doRefreshToken(rt: string): Promise { tokenRefreshPromise = (async () => { try { const res = await fetch('/api/v1/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: rt }), }) if (!res.ok) throw new Error('Refresh failed') const data = await res.json() const newToken = data.data.token const newRt = data.data.refresh_token localStorage.setItem('token', newToken) localStorage.setItem('refresh_token', newRt) return newToken } finally { tokenRefreshPromise = null } })() return tokenRefreshPromise } async function apiFetch(url: string, init?: RequestInit): Promise { const headers: Record = { ...(init?.headers as Record || {}) } const storedToken = localStorage.getItem('token') if (storedToken) { headers['Authorization'] = `Bearer ${storedToken}` } // FormData 时不要设 Content-Type if (!(init?.body instanceof FormData)) { headers['Content-Type'] = headers['Content-Type'] || 'application/json' } let res = await fetch(url, { ...init, headers }) // 滑动过期:检测 X-New-Token 响应头 const newToken = res.headers.get('X-New-Token') if (newToken) { localStorage.setItem('token', newToken) } // 401 时尝试用 refresh_token 刷新一次 if (res.status === 401 && storedToken) { const storedRt = localStorage.getItem('refresh_token') if (storedRt) { try { const freshToken = await doRefreshToken(storedRt) headers['Authorization'] = `Bearer ${freshToken}` res = await fetch(url, { ...init, headers }) } catch { localStorage.removeItem('token') localStorage.removeItem('refresh_token') localStorage.removeItem('user') window.location.href = '/login' } } } if (!res.ok) { try { const err = await res.json() throw new Error(err.detail || err.error || err.message || `Request failed: ${res.status}`) } catch (e) { if (e instanceof Error) throw e throw new Error(`Request failed: ${res.status}`) } } return res } const api = { // --- Health --- health: () => apiFetch('/health').then(r => r.json()), // --- Models --- models: { list: () => apiFetch('/api/v1/models/').then(r => r.json()) as Promise, download: (modelId: string, useModelscope = false) => apiFetch('/api/v1/models/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_id: modelId, use_modelscope: useModelscope }), }).then(r => r.json()) as Promise, downloadStatus: (taskId: string) => apiFetch(`/api/v1/models/download/${taskId}`).then(r => r.json()) as Promise, listDownloads: () => apiFetch('/api/v1/models/downloads').then(r => r.json()) as Promise, cancelDownload: (taskId: string) => apiFetch(`/api/v1/models/download/${taskId}/cancel`, { method: 'POST' }).then(r => r.json()), delete: (modelId: string) => apiFetch(`/api/v1/models/${encodeURIComponent(modelId)}`, { method: 'DELETE' }).then(r => r.json()), getInfo: (modelId: string) => apiFetch(`/api/v1/models/${encodeURIComponent(modelId)}`).then(r => r.json()) as Promise, test: (req: ModelTestRequest) => apiFetch('/api/v1/models/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req), }).then(r => r.json()) as Promise, }, // --- Datasets --- datasets: { list: () => apiFetch('/api/v1/datasets/').then(r => r.json()) as Promise, upload: (file: File) => { const form = new FormData() form.append('file', file) return apiFetch('/api/v1/datasets/upload', { method: 'POST', body: form }).then(r => r.json()) as Promise }, download: (datasetId: string, useModelscope = false) => apiFetch('/api/v1/datasets/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dataset_id: datasetId, use_modelscope: useModelscope }), }).then(r => r.json()) as Promise, downloadStatus: (taskId: string) => apiFetch(`/api/v1/datasets/download/${taskId}`).then(r => r.json()) as Promise, listDownloads: () => apiFetch('/api/v1/datasets/downloads').then(r => r.json()) as Promise, cancelDownload: (taskId: string) => apiFetch(`/api/v1/datasets/download/${taskId}/cancel`, { method: 'POST' }).then(r => r.json()), preview: (id: string, rows = 10) => apiFetch(`/api/v1/datasets/${id}/preview?rows=${rows}`).then(r => r.json()) as Promise, validate: (id: string) => apiFetch(`/api/v1/datasets/${id}/validate`, { method: 'POST' }).then(r => r.json()) as Promise, delete: (id: string) => apiFetch(`/api/v1/datasets/${id}`, { method: 'DELETE' }).then(r => r.json()), }, // --- Training --- training: { list: () => apiFetch('/api/v1/training/jobs').then(r => r.json()) as Promise, create: (cfg: TrainingConfig) => apiFetch('/api/v1/training/jobs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg), }).then(r => r.json()) as Promise, get: (id: string) => apiFetch(`/api/v1/training/jobs/${id}`).then(r => r.json()) as Promise, cancel: (id: string) => apiFetch(`/api/v1/training/jobs/${id}/cancel`, { method: 'POST' }).then(r => r.json()), }, // --- Evaluation --- evaluation: { run: (cfg: EvalConfig) => apiFetch('/api/v1/evaluation/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg), }).then(r => r.json()) as Promise, results: (id: string) => apiFetch(`/api/v1/evaluation/${id}/results`).then(r => r.json()) as Promise, }, // --- Deployment --- deployment: { export: (cfg: DeployConfig) => apiFetch('/api/v1/deployment/export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg), }).then(r => r.json()) as Promise, status: (id: string) => apiFetch(`/api/v1/deployment/${id}/status`).then(r => r.json()) as Promise, }, // --- Sample Center --- sampleCenter: { listKnowledgeBases: (page = 1, page_size = 20) => apiFetch(`/api/v1/sample-center/knowledge-bases?page=${page}&page_size=${page_size}`) .then(r => r.json()) as Promise, getKnowledgeBaseDetail: (kb_id: string) => apiFetch(`/api/v1/sample-center/knowledge-bases/${kb_id}`) .then(r => r.json()) as Promise, importFromKnowledgeBase: (kb_id: string, kb_name = '') => apiFetch(`/api/v1/sample-center/knowledge-bases/${kb_id}/import?kb_name=${encodeURIComponent(kb_name)}`, { method: 'POST', }).then(r => r.json()) as Promise, }, // --- Inference --- inference: { generate: (req: InferenceRequest) => apiFetch('/api/v1/inference/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req), }).then(r => r.json()) as Promise, adapters: () => apiFetch('/api/v1/inference/adapters').then(r => r.json()) as Promise, }, } export default api // --- Types --- interface ModelInfo { id: string name: string model_type: string path?: string is_downloaded: boolean context_length?: number supported_peft_methods: string[] } interface ModelTestRequest { model_id: string prompt: string max_new_tokens?: number temperature?: number top_p?: number } interface ModelTestResponse { model_id: string prompt: string generated_text: string error?: string } interface ModelDownloadResponse { model_id: string status: string path?: string error?: string } interface ModelDownloadTaskResponse { task_id: string model_id: string status: string use_modelscope?: boolean path?: string error?: string progress?: number created_at?: string } interface DatasetInfo { id: string name: string format: string record_count: number file_path: string created_at: string } interface DatasetDownloadResponse { dataset_id: string status: string path?: string error?: string } interface DatasetDownloadTaskResponse { task_id: string dataset_id: string status: string use_modelscope?: boolean path?: string error?: string record_count?: number created_at?: string } interface DatasetPreview { total_records: number preview_rows: { row_index: number; data: Record }[] columns: string[] } interface DatasetValidation { is_valid: boolean errors?: string[] warnings?: string[] } interface TrainingJob { id: string model_id: string model_type: string peft_method: string status: string progress: number current_epoch: number current_step: number total_steps: number loss?: number created_at: string started_at?: string finished_at?: string error_message?: string adapter_path?: string } interface TrainingConfig { model_id: string model_type: string dataset_id: string peft_method: string task_type?: string dataset_template?: string epochs?: number batch_size?: number gradient_accumulation?: number learning_rate?: number max_seq_length?: number warmup_ratio?: number save_strategy?: string eval_strategy?: string eval_steps?: number lora_r?: number lora_alpha?: number lora_dropout?: number lora_target_modules?: string qlora_bits?: number deepspeed?: boolean num_gpus?: number } interface EvalConfig { job_id: string test_split_ratio?: number batch_size?: number metrics?: string[] } interface EvalResult { id: string job_id: string status: string progress: number metrics: Record error: string | null created_at: string } interface DeployConfig { job_id: string merge_with_base?: boolean export_format?: string } interface DeployResponse { job_id: string status: string output_path?: string error?: string } interface AdapterInfo { id: string path: string base_model: string peft_type: string model_id?: string peft_method?: string task_type?: string created_at?: string } interface InferenceRequest { adapter_id: string prompt: string max_new_tokens?: number temperature?: number top_p?: number repetition_penalty?: number do_sample?: boolean } interface InferenceResponse { prompt: string generated_text: string generated_only: string tokens_generated: number error?: string } interface MetadataSchemaField { field_name_cn: string field_name_en: string field_type: string description: string } interface KnowledgeBaseItem { id: string name: string parent_table: string child_table: string document_count: number status: number created_at: string created_by: string metadata_schema: MetadataSchemaField[] } interface KnowledgeBaseListResponse { total: number page: number page_size: number items: KnowledgeBaseItem[] } interface KnowledgeBaseDetailResponse extends KnowledgeBaseItem { description: string updated_at: string } interface KbImportResponse { kb_id: string kb_name: string document_count: number metadata_schema: MetadataSchemaField[] parent_table: string child_table: string } export type { ModelInfo, ModelTestRequest, ModelTestResponse, ModelDownloadResponse, ModelDownloadTaskResponse, DatasetInfo, DatasetDownloadResponse, DatasetDownloadTaskResponse, DatasetPreview, DatasetValidation, TrainingJob, TrainingConfig, EvalConfig, EvalResult, DeployConfig, DeployResponse, AdapterInfo, InferenceRequest, InferenceResponse, KnowledgeBaseItem, KnowledgeBaseListResponse, KnowledgeBaseDetailResponse, KbImportResponse }