import { API_BASE_URL, type ApiResponse, CLIENT_ID, CLIENT_SECRET, } from "@/lib/api" import { clearSession, getUserToken } from "./token" type CallMode = "public" | "device" | "user" async function request( endpoint: string, init: { method: "GET" | "POST" body?: string auth?: string }, ): Promise> { try { const headers: HeadersInit = {} if (init.body !== undefined) { headers["Content-Type"] = "application/json" } if (init.auth) headers.Authorization = init.auth const response = await fetch(`${API_BASE_URL}${endpoint}`, { method: init.method, headers, body: init.body, }) 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 R } } if (contentType.includes("application/json")) { const json = (await response.json()) as R & { message?: string error_description?: string Code?: number Message?: string Data?: unknown data?: unknown } if (!response.ok) { return { success: false, status: response.status, message: json.message || json.error_description || "请求失败", } } if (typeof json.Code === "number") { if (json.Code === 10000) { return { success: true, data: (json.Data ?? json.data ?? undefined) as R, } } return { success: false, message: json.Message || "请求失败", } } return { success: true, data: json } } throw new Error(`无法解析响应数据: ${contentType}`) } catch (e) { console.error("后端请求异常:", e) return { success: false, status: 500, message: (e as Error).message || "网络错误", } } } let deviceToken: string | null = null let deviceTokenExpire = 0 let deviceTokenPromise: Promise | null = null async function acquireDeviceToken(): Promise { if (deviceToken && Date.now() < deviceTokenExpire) return deviceToken if (!deviceTokenPromise) { const basic = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`) deviceTokenPromise = request<{ access_token: string expires_in: number }>("/api/auth/token", { method: "POST", body: JSON.stringify({ grant_type: "client_credentials" }), auth: `Basic ${basic}`, }) .then(resp => { if (!resp.success) { deviceToken = null return null } deviceToken = resp.data.access_token deviceTokenExpire = Date.now() + (resp.data.expires_in - 60) * 1000 return resp.data.access_token }) .finally(() => { deviceTokenPromise = null }) } return deviceTokenPromise } const inflight = new Map>>() function dedupGet( endpoint: string, run: () => Promise>, ): Promise> { const existing = inflight.get(endpoint) if (existing) return existing as Promise> const promise = run().finally(() => inflight.delete(endpoint)) inflight.set(endpoint, promise) return promise } async function call( mode: CallMode, endpoint: string, data?: unknown, method: "GET" | "POST" = "POST", ): Promise> { const body = data === undefined ? undefined : JSON.stringify(data) const run = async (): Promise> => { let auth: string | undefined if (mode === "user") { const token = getUserToken() if (!token) { return { success: false, status: 401, message: "未登录或会话已过期", } } auth = `Bearer ${token}` } else if (mode === "device") { const token = await acquireDeviceToken() if (!token) { return { success: false, status: 401, message: "设备凭证获取失败", } } auth = `Bearer ${token}` } const resp = await request(endpoint, { method, body, auth }) if (!resp.success && resp.status === 401) { if (mode === "user") { clearSession() } else if (mode === "device") { deviceToken = null deviceTokenExpire = 0 } } return resp } if (method === "GET" && data === undefined) { return dedupGet(endpoint, run) } return run() } async function callPublic( endpoint: string, data?: unknown, method: "GET" | "POST" = "POST", ): Promise> { return call("public", endpoint, data, method) } async function callByDevice( endpoint: string, data?: unknown, method: "GET" | "POST" = "POST", ): Promise> { return call("device", endpoint, data, method) } async function callByUser( endpoint: string, data?: unknown, method: "GET" | "POST" = "POST", ): Promise> { return call("user", endpoint, data, method) } export { callByDevice, callByUser, callPublic }