import { getToken, clearAuth } from './adminAuth'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'; export class ApiError extends Error { status: number; constructor(message: string, status: number) { super(message); this.status = status; } } function parseFilenameFromContentDisposition(contentDisposition: string | null): string | null { if (!contentDisposition) return null; // Prefer RFC 5987 filename*=UTF-8''... const filenameStarMatch = contentDisposition.match(/filename\*=(?:UTF-8''|utf-8'')([^;]+)/); if (filenameStarMatch?.[1]) { try { return decodeURIComponent(filenameStarMatch[1].trim().replace(/^"|"$/g, '')); } catch { return filenameStarMatch[1].trim().replace(/^"|"$/g, ''); } } const filenameMatch = contentDisposition.match(/filename=([^;]+)/); if (filenameMatch?.[1]) { return filenameMatch[1].trim().replace(/^"|"$/g, ''); } return null; } async function request(path: string, init?: RequestInit): Promise { const token = getToken(); const headers: Record = { 'Content-Type': 'application/json', }; if (token) { headers['Authorization'] = `Bearer ${token}`; } const res = await fetch(`${API_BASE_URL}${path}`, { ...init, headers: { ...headers, ...(init?.headers || {}), }, credentials: 'include', }); if (res.status === 401) { clearAuth(); } const data = (await res.json().catch(() => ({}))) as any; if (!res.ok) { throw new ApiError(data?.message || `请求失败: ${res.status}`, res.status); } return data as T; } async function requestDownload(path: string, init?: RequestInit): Promise<{ blob: Blob; filename: string }> { const token = getToken(); const headers: Record = { ...(init?.headers as any), }; // Do NOT force Content-Type for downloads (GET binary) if (token) { headers['Authorization'] = `Bearer ${token}`; } const res = await fetch(`${API_BASE_URL}${path}`, { ...init, headers, credentials: 'include', }); if (res.status === 401) { clearAuth(); } if (!res.ok) { const text = await res.text().catch(() => ''); // Try to extract message from JSON-ish body let message = `请求失败: ${res.status}`; try { const maybeJson = JSON.parse(text || '{}'); message = maybeJson?.message || message; } catch { // ignore } throw new ApiError(message, res.status); } const filename = parseFilenameFromContentDisposition(res.headers.get('content-disposition')) || 'logs.tar.gz'; const blob = await res.blob(); return { blob, filename }; } export const api = { adminLogin: (identifier: string, password: string) => request('/admin/auth/login', { method: 'POST', body: JSON.stringify({ identifier, password }), }), listUsers: (limit = 100, offset = 0) => request(`/admin/users?limit=${encodeURIComponent(limit)}&offset=${encodeURIComponent(offset)}`), resetUserPassword: (userId: string, newPassword: string) => request(`/admin/users/${encodeURIComponent(userId)}/reset-password`, { method: 'POST', body: JSON.stringify({ new_password: newPassword }), }), getRuntimeLogs: (lines = 200) => request(`/admin/logs/runtime?lines=${encodeURIComponent(lines)}`), downloadLogsArchive: () => requestDownload('/admin/logs/archive'), };