import { API_BASE_URL, type ApiResponse, CLIENT_ID, CLIENT_SECRET, UnauthorizedError, } from "@/lib/api" let deviceToken: string | null = null let deviceTokenExpire: Date | null = null async function call( endpoint: string, body?: string, auth?: string, ): Promise> { try { const headers: HeadersInit = { "Content-Type": "application/json", } if (auth) headers["Authorization"] = auth const response = await fetch(`${API_BASE_URL}${endpoint}`, { method: "POST", headers, body, }) if (response.status === 401) { // 如果是 刷新Token 的接口本身报 401,说明 RefreshToken 彻底失效,直接抛出错误 if ( endpoint.includes("/auth/token") || endpoint.includes("/auth/revoke") ) { throw UnauthorizedError } // 动态引入 auth.ts 里的刷新函数,避免循环引用报错 const { refreshAuth } = await import("./auth") // 尝试刷新 Token const newTokens = await refreshAuth() if (newTokens) { // 刷新成功,用新 Token 重试当前请求(递归调用) return call(endpoint, body, `Bearer ${newTokens.access_token}`) } else { // 刷新失败,抛出 401 错误,让上层处理跳转 throw UnauthorizedError } } const contentType = response.headers.get("Content-Type") ?? "text/plain" if (contentType.includes("text/plain")) { const text = await response.text() if (!response.ok) { return { success: false, status: response.status, message: text || "请求失败", } } return { success: true, data: undefined as T } } if (contentType.includes("application/json")) { const json = await response.json() if (!response.ok) { return { success: false, status: response.status, message: json.message || json.error_description || "请求失败", } } return { success: true, data: json } } throw new Error(`无法解析响应数据: ${contentType}`) } catch (e) { console.error("后端请求异常:", e) return { success: false, status: 500, message: (e as Error).message || "网络错误", } } } // ====================== // Public // ====================== async function callPublic( endpoint: string, data?: unknown, ): Promise> { return call(endpoint, data ? JSON.stringify(data) : undefined) } // ====================== // Device // ====================== async function callByDevice( endpoint: string, data?: unknown, ): Promise> { // 检查内存中的 Token 是否过期 if (!deviceToken || !deviceTokenExpire || new Date() >= deviceTokenExpire) { // 用 btoa 生成 Basic 认证头 const basic = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`) const resp = await call<{ access_token: string expires_in: number }>( "/api/auth/token", JSON.stringify({ grant_type: "client_credentials" }), `Basic ${basic}`, ) if (!resp.success) { return resp } deviceToken = resp.data.access_token // 提前 60 秒过期,防止临界点失效 deviceTokenExpire = new Date( Date.now() + (resp.data.expires_in - 60) * 1000, ) } return call( endpoint, data ? JSON.stringify(data) : undefined, `Bearer ${deviceToken}`, ) } // ====================== // User // ====================== async function callByUser( endpoint: string, data?: unknown, ): Promise> { // 这里我们假设用户登录后的 Token 存在 localStorage 里 // 如果你是用 Cookie 存 Token,这里用 js-cookie 的 get 方法替换即可 const token = localStorage.getItem("auth_token") if (!token) { throw UnauthorizedError } return call( endpoint, data ? JSON.stringify(data) : undefined, `Bearer ${token}`, ) } export { callByDevice, callByUser, callPublic }