// 获取 base URL(从后端注入的全局变量) const BASE_URL = (window as any).__BASE_URL__ || '' const API_VERSION = (window as any).__API_VERSION__ || '/api/v1' const API_BASE_URL = BASE_URL + API_VERSION interface ApiResponse { code: number msg: string data: T } export interface MonitorStats { env: { os: string arch: string go_version: string num_cpu: number goroutines: number } host: { cpu_percent: number mem_total: number mem_used: number mem_percent: number disk_total: number disk_used: number disk_percent: number uptime: number platform: string } mem: { alloc: number total_alloc: number sys: number lookups: number mallocs: number frees: number } heap: { heap_alloc: number heap_sys: number heap_idle: number heap_inuse: number heap_released: number heap_objects: number } gc: { next_gc: number last_gc: number pause_total_ns: number num_gc: number } scheduler: { scheduled: number running: number queue_size: number worker_count: number workers: { id: number status: string task_id?: string task_name?: string start_time?: number duration?: number }[] } } // 初始化时使用空字符串,避免 SSR 时访问 localStorage export let activeInterconnectNodeId = '' export let activeInterconnectNodeName = '' // 在客户端初始化时从 localStorage 读取 if (typeof window !== 'undefined') { activeInterconnectNodeId = localStorage.getItem('activeInterconnectNodeId') || '' activeInterconnectNodeName = localStorage.getItem('activeInterconnectNodeName') || '' } export function setActiveInterconnectNodeId(id: string, name?: string) { activeInterconnectNodeId = id if (name !== undefined) { activeInterconnectNodeName = name } localStorage.removeItem('site_settings_cache') if (id) { localStorage.setItem('activeInterconnectNodeId', id) if (name) localStorage.setItem('activeInterconnectNodeName', name) document.cookie = `active_interconnect_node_id=${id}; path=/; max-age=${7 * 24 * 3600}` } else { localStorage.removeItem('activeInterconnectNodeId') localStorage.removeItem('activeInterconnectNodeName') activeInterconnectNodeName = '' document.cookie = `active_interconnect_node_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC` } } let redirectingToLogin = false function redirectToLogin() { const loginPath = `${BASE_URL}/login` if (redirectingToLogin) return if (window.location.pathname === `${BASE_URL}/login` || window.location.pathname.endsWith('/login')) return redirectingToLogin = true window.location.href = loginPath } async function parseApiResponse(res: Response, url: string): Promise> { const text = await res.text() const trimmed = text.trim() if (!trimmed) { if (res.status === 502 || res.status === 503 || res.status === 504) { throw new Error('后端服务不可用,请确认已启动 TaskPool server(默认 :8052)') } if (!res.ok) { throw new Error(`请求失败(HTTP ${res.status})`) } throw new Error('服务器返回了空响应') } let json: ApiResponse try { json = JSON.parse(trimmed) as ApiResponse } catch { if (res.status === 502 || res.status === 503 || res.status === 504) { throw new Error('后端服务不可用,请确认已启动 TaskPool server(默认 :8052)') } throw new Error(`服务器返回了非 JSON 响应(HTTP ${res.status})`) } if (json.code === 401) { // 公开接口不应触发硬跳转;其它接口未登录时跳转登录页 const isPublic = url.startsWith('/settings/public') || url.startsWith('/auth/login') || url.startsWith('/install') if (!isPublic) redirectToLogin() throw new Error(json.msg || '请先登录') } if (typeof json.code === 'number' && json.code !== 200) { throw new Error(json.msg || '请求失败') } // 兼容非标准成功体:HTTP 非 2xx 时优先报状态码 if (!res.ok && (json as any)?.code === undefined) { throw new Error(json.msg || `请求失败(HTTP ${res.status})`) } return json } export async function request(url: string, options?: RequestInit): Promise { let res: Response try { res = await fetch(`${API_BASE_URL}${url}`, { ...options, credentials: 'include', // 携带 Cookie headers: { 'Content-Type': 'application/json', ...options?.headers, }, }) } catch (err: any) { // 路由切换 / React Query 取消请求时,浏览器会报 net::ERR_ABORTED if (err?.name === 'AbortError' || options?.signal?.aborted) { throw err } throw new Error(err?.message || '网络请求失败,请检查后端是否已启动') } const json = await parseApiResponse(res, url) return json.data } // 检查登录状态(不触发自动跳转) export async function checkAuth(): Promise { try { const res = await fetch(`${API_BASE_URL}/auth/me`, { credentials: 'include', headers: { 'Content-Type': 'application/json' }, }) const text = await res.text() if (!text.trim()) return false const json = JSON.parse(text) as ApiResponse<{ username: string }> return json.code === 200 } catch { return false } } export const api = { auth: { login: (data: { username: string; password: string }) => request<{ user: string }>('/auth/login', { method: 'POST', body: JSON.stringify(data) }), logout: () => request('/auth/logout', { method: 'POST' }), me: () => request<{ username: string; role: string }>('/auth/me'), register: (data: { username: string; password: string; email: string }) => request('/auth/register', { method: 'POST', body: JSON.stringify(data) }) }, tasks: { list: (params?: { page?: number; page_size?: number; name?: string; agent_id?: string; tags?: string; type?: string; sort_by?: string; order?: string }) => { const query = new URLSearchParams() if (params?.page) query.set('page', String(params.page)) if (params?.page_size) query.set('page_size', String(params.page_size)) if (params?.name) query.set('name', params.name) if (params?.tags) query.set('tags', params.tags) if (params?.agent_id) query.set('agent_id', params.agent_id) if (params?.type) query.set('type', params.type) if (params?.sort_by) query.set('sort_by', params.sort_by) if (params?.order) query.set('order', params.order) return request(`/tasks?${query}`) }, create: (data: Partial) => request('/tasks', { method: 'POST', body: JSON.stringify(data) }), update: (id: string, data: Partial) => request(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(data) }), delete: (id: string, params?: { delete_files?: boolean }) => { const query = new URLSearchParams() if (params?.delete_files !== undefined) query.set('delete_files', String(params.delete_files)) const queryString = query.toString() return request(`/tasks/${id}${queryString ? '?' + queryString : ''}`, { method: 'DELETE' }) }, batchDelete: (ids: string[]) => request<{ count: number }>('/tasks/batch-delete', { method: 'POST', body: JSON.stringify({ ids }) }), batchDeleteByQuery: (params?: { name?: string, agent_id?: string, tags?: string, type?: string }) => { const query = new URLSearchParams() if (params?.name) query.append('name', params.name) if (params?.agent_id) query.append('agent_id', params.agent_id) if (params?.tags) query.append('tags', params.tags) if (params?.type && params.type !== 'all') query.append('type', params.type) return request<{ count: number }>(`/tasks/batch-by-query?${query.toString()}`, { method: 'DELETE' }) }, execute: (id: string) => request(`/execute/task/${id}`, { method: 'POST' }), stop: (logID: string) => request(`/tasks/stop/${logID}`, { method: 'POST' }), tags: () => request('/tasks/tags') }, scripts: { list: () => request('/scripts'), create: (data: Partial