/** * Admin 后台管理系统 - 统一 API 客户端 * * 写入型/高成本接口(采集、回测、结算)受 X-API-Key 保护: * 密钥在「系统配置」页设置,存于本机 localStorage,每次请求自动附带。 */ const API_BASE = '/api/v1' const TIMEOUT_MS = 30_000 const ADMIN_KEY_STORAGE = 'profeto_admin_key' /** 读取本机保存的管理员密钥 */ export function getAdminKey(): string { try { return localStorage.getItem(ADMIN_KEY_STORAGE) ?? '' } catch { return '' } } /** 保存/清除管理员密钥(传空字符串即清除) */ export function setAdminKey(key: string): void { try { if (key) localStorage.setItem(ADMIN_KEY_STORAGE, key) else localStorage.removeItem(ADMIN_KEY_STORAGE) } catch { /* 隐私模式等场景下不可用,静默忽略 */ } } export class ApiError extends Error { constructor( message: string, public status: number, public data?: unknown, ) { super(message) this.name = 'ApiError' } } async function request(path: string, options: RequestInit = {}): Promise { // 修复: 正确拼接 API_BASE const url = path.startsWith('http') ? path : path.startsWith('/') ? path // 已经是绝对路径(如 /health) : `${API_BASE}${path}` const controller = new AbortController() const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) try { const adminKey = getAdminKey() const res = await fetch(url, { ...options, signal: controller.signal, headers: { 'Content-Type': 'application/json', ...(adminKey ? { 'X-API-Key': adminKey } : {}), ...options.headers, }, }) if (!res.ok) { let detail: unknown try { detail = await res.json() } catch { detail = await res.text() } let message = detail && typeof detail === 'object' && 'detail' in detail ? String((detail as { detail: unknown }).detail) : `HTTP ${res.status}: ${res.statusText}` if (res.status === 401) { message += '\n请在「系统配置」页填写管理员密钥后重试。' } throw new ApiError(message, res.status, detail) } // 修复: 正确判断 204 No Content if (res.status === 204) { return undefined as T } return res.json() as Promise } catch (err) { if (err instanceof ApiError) throw err if (err instanceof DOMException && err.name === 'AbortError') { throw new ApiError('请求超时,请稍后重试', 0) } throw new ApiError( err instanceof Error ? err.message : '网络错误,请检查连接', 0, ) } finally { clearTimeout(timer) } } export const api = { get: (path: string) => request(path), post: (path: string, body?: unknown) => request(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined }), put: (path: string, body?: unknown) => request(path, { method: 'PUT', body: body ? JSON.stringify(body) : undefined }), delete: (path: string) => request(path, { method: 'DELETE' }), } export { API_BASE }