/** 登录身份和预览身份分别传递;切换视角会取消旧视角的在途请求。 */ let tokenGetter: () => string | null = () => null; let previewTokenGetter: () => string | null = () => null; let identityAbort = new AbortController(); let identityVersion = 0; export function setTokenGetter(fn: () => string | null) { tokenGetter = fn; } export function setPreviewTokenGetter(fn: () => string | null) { previewTokenGetter = fn; } export function invalidateAuthenticatedRequests() { identityVersion += 1; identityAbort.abort(); identityAbort = new AbortController(); } const expiredPreviewCodes = new Set(['PREVIEW_EXPIRED', 'PREVIEW_REVOKED', 'PREVIEW_INVALID', 'PREVIEW_TARGET_UNAVAILABLE']); async function authenticatedRequest(url: string, options: RequestInit | undefined, preview: boolean, business: boolean): Promise { const version = identityVersion; const headers = new Headers(options?.headers); const token = tokenGetter(); if (token) headers.set('Authorization', `Bearer ${token}`); headers.delete('X-BI-Preview'); const previewToken = preview ? previewTokenGetter() : null; if (previewToken) headers.set('X-BI-Preview', previewToken); const signal = business ? options?.signal ? AbortSignal.any([options.signal, identityAbort.signal]) : identityAbort.signal : options?.signal; const res = await fetch(url, { ...options, headers, signal }); if (business && version !== identityVersion) throw new DOMException('权限视角已切换', 'AbortError'); if (res.status === 401 || res.status === 403 || res.status === 410) { const body = await res.clone().json().catch(() => ({})); if (business && version !== identityVersion) throw new DOMException('权限视角已切换', 'AbortError'); if (expiredPreviewCodes.has(body.code)) { // 已切换到其他人的旧响应不能结束新预览。 if (previewToken && previewToken === previewTokenGetter()) window.dispatchEvent(new CustomEvent('auth:preview-ended', { detail: body.code })); throw new Error('权限视角已失效,请重新选择用户'); } if (res.status === 401) { window.dispatchEvent(new CustomEvent('auth:unauthorized')); throw new Error('Unauthorized'); } if (body.code === 'PREVIEW_READ_ONLY') { window.dispatchEvent(new CustomEvent('auth:preview-readonly')); throw new Error('权限视角为只读,请退出预览后操作'); } } return res; } export function fetchAuthenticated(url: string, options?: RequestInit): Promise { return authenticatedRequest(url, options, true, true); } /** 管理接口始终使用实际登录身份;仅 current/stop 附带当前预览凭证。 */ export async function fetchPreviewControl(path: string, options?: RequestInit, includePreview = false): Promise { const res = await authenticatedRequest(`/api/permission-preview/${path}`, options, includePreview, false); const data = await res.json(); if (!res.ok) throw new Error(data.error || data.message || '权限视角请求失败,请重试'); return data as T; } export async function fetchJson(url: string, options?: RequestInit): Promise { const res = await fetchAuthenticated(url, options); if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`); return res.json(); }