type ApiEnv = { NEXT_PUBLIC_API_URL?: string; NODE_ENV?: string; }; export function resolveApiBase(env: ApiEnv = process.env) { const fallback = env.NODE_ENV === 'production' ? '/api/v1' : 'http://localhost:3001/api/v1'; const configured = (env.NEXT_PUBLIC_API_URL || fallback).replace(/\/$/, ''); if (env.NODE_ENV === 'production' && isLocalOnlyApiUrl(configured)) { return '/api/v1'; } return configured; } const API_BASE = resolveApiBase(); function isLocalOnlyApiUrl(value: string) { try { const url = new URL(value); return ['localhost', '127.0.0.1', '0.0.0.0'].includes(url.hostname); } catch { return false; } } let apiAvailable: boolean | null = null; let probePromise: Promise | null = null; function getErrorMessage(body: unknown, status: number) { if (typeof body === 'object' && body !== null && 'message' in body) { const message = (body as { message?: unknown }).message; if (typeof message === 'string') return message; if (Array.isArray(message)) return message.join(', '); } return `Request failed: ${status}`; } export class ApiRequestError extends Error { constructor( public readonly status: number, public readonly body: unknown, ) { super(getErrorMessage(body, status)); this.name = 'ApiRequestError'; } } async function checkApi(): Promise { if (apiAvailable === true) return true; if (probePromise) return probePromise; probePromise = (async () => { try { // 探测一个不依赖数据库的端点(/config/ai 总是返回 200,只要 NestJS 起来了) const res = await fetch(`${API_BASE}/config/ai`, { method: 'GET', signal: AbortSignal.timeout(1500) }); apiAvailable = res.ok ? true : null; } catch { apiAvailable = null; } finally { probePromise = null; } return apiAvailable === true; })(); return probePromise; } async function request(path: string, options?: RequestInit): Promise { const available = await checkApi(); if (!available) throw new Error('API 不可用'); const res = await fetch(`${API_BASE}${path}`, { headers: { 'Content-Type': 'application/json', ...options?.headers }, ...options, }).catch(() => null); if (!res) { apiAvailable = null; throw new Error('API 不可用'); } if (!res.ok) { const error = await res.json().catch(() => ({})); throw new ApiRequestError(res.status, error); } return res.json(); } export const api = { get: (path: string) => request(path), post: (path: string, data: unknown) => request(path, { method: 'POST', body: JSON.stringify(data) }), put: (path: string, data: unknown) => request(path, { method: 'PUT', body: JSON.stringify(data) }), patch: (path: string, data: unknown) => request(path, { method: 'PATCH', body: JSON.stringify(data) }), delete: (path: string) => request(path, { method: 'DELETE' }), postRaw: async (path: string, data: unknown, timeoutMs = 120000): Promise => { // 调用 AI 类长耗时接口时使用,跳过 checkApi 短路(确保走真实请求) const controller = new AbortController(); const tid = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetch(`${API_BASE}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), signal: controller.signal, }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new ApiRequestError(res.status, err); } return res.json(); } finally { clearTimeout(tid); } }, }; export function __resetApiAvailabilityForTests() { apiAvailable = null; probePromise = null; }