import { getPublicApiOrigin } from '@/lib/env'; import { emit401Unauthorized, emit403Forbidden } from '@/lib/notify/auth-events'; import { refreshTokensShared } from '@/lib/api/refresh-flight'; import { ApiError, type ApiEnvelope } from '@/lib/api/types'; type GetTokens = () => { accessToken: string | null; refreshToken: string | null }; type SetTokens = (access: string | null, refresh: string | null) => void; let tokenBridge: { get: GetTokens; set: SetTokens } | null = null; /** 由 auth store 在客户端挂载时注册,避免循环依赖。 */ export function registerTokenBridge(get: GetTokens, set: SetTokens) { tokenBridge = { get, set }; } let tenantIdGetter: () => string | null = () => null; /** 由 tenant store 注册,请求 `/api/v1` 时附加 `X-Tenant-ID`(可选)。 */ export function registerTenantHeaderBridge(get: () => string | null) { tenantIdGetter = get; } function authHeader(): HeadersInit { const t = tokenBridge?.get().accessToken; return t ? { Authorization: `Bearer ${t}` } : {}; } function tenantHeader(): HeadersInit { const tid = tenantIdGetter?.(); return tid ? { 'X-Tenant-ID': tid } : {}; } /** 业务 JSON:HTTP 200 时解析 envelope;401 时共享 refresh 后重试一次(登录请求勿走此逻辑)。 */ export async function apiJson( path: string, init: RequestInit & { skipRefreshRetry?: boolean } = {} ): Promise { const { skipRefreshRetry, ...req } = init; const url = `${getPublicApiOrigin()}${path.startsWith('/') ? path : `/${path}`}`; const method = (req.method || 'GET').toUpperCase(); const hasBody = req.body != null && req.body !== ''; const jsonHeaders: HeadersInit = method !== 'GET' && method !== 'HEAD' && hasBody ? { 'Content-Type': 'application/json' } : {}; const baseHeaders = { ...jsonHeaders, ...authHeader(), ...tenantHeader(), ...(req.headers || {}), }; const first = await fetch(url, { ...req, headers: baseHeaders, credentials: 'include', }); if (first.status === 401 && !skipRefreshRetry && tokenBridge) { const ok = await refreshTokensShared( () => tokenBridge!.get().refreshToken, (a, r) => tokenBridge!.set(a, r) ); if (ok) { const second = await fetch(url, { ...req, headers: { ...jsonHeaders, ...authHeader(), ...tenantHeader(), ...(req.headers || {}), }, credentials: 'include', }); return handleEnvelope(second); } } return handleEnvelope(first); } async function handleEnvelope(res: Response): Promise { const text = await res.text(); let body: unknown = null; try { body = text ? JSON.parse(text) : null; } catch { throw new ApiError('响应不是合法 JSON', res.status, text); } if (res.status === 401) { const msg = (body as { msg?: string })?.msg || '未授权'; emit401Unauthorized(msg); throw new ApiError(msg, 401, body); } if (res.status === 403) { const msg = (body as { msg?: string })?.msg || '禁止访问'; emit403Forbidden(msg); throw new ApiError(msg, 403, body); } if (!res.ok) { throw new ApiError(`HTTP ${res.status}`, res.status, body); } const env = body as ApiEnvelope; if (typeof env?.code !== 'number') { return body as T; } if (env.code !== 200) { throw new ApiError(env.msg || '业务失败', res.status, env); } return env.data as T; }