import { ApiError, decodeEnvelope } from "./protocol"; import type { Envelope, HeaderNavigation, HomeData } from "./types"; import { cache } from "react"; import { headers } from 'next/headers'; import { publicCachePolicy, publicDataCache } from './public-cache'; import { getApiBaseUrl, usesCurrentApiOrigin, UPSTREAM_HOP_HEADER, type ApiRequestContext } from './config'; export { DEFAULT_API_BASE_URL } from './config'; export const PUBLIC_PATHS = new Set([ "playlet/home", "config/info", "config/footer/info", "config/richtext/list", "navigation/header", "theme/list", "playlet/tag/options", "translation/category/list", "applist/list", "playlet/all/list", "playlet/hot/list", "playlet/hot/rank/list", "playlet/topic/list", "playlet/topic/detail", "playlet/tag/list", "playlet/guess-like/list", "playlet/hot-like", ]); const GET_PATHS = [ ...PUBLIC_PATHS, "auth/getInfo", "playlet/detail", "playlet/chapter/list", "playlet/chapter/detail", "playlet/search", "playlet/search/suggest", ]; const POST_PATHS = [ "auth/guest", "playlet/collect/set", "playlet/like/set", "playlet/chapter/collect/set", "playlet/chapter/like/set", "playlet/chapter/watch/report", "playlet/forward/heat", "ads/click", ]; export function allowedRequest(path: string, method: string): boolean { return (method === "GET" ? GET_PATHS : method === "POST" ? POST_PATHS : []).includes(path); } export async function getServerApiBaseUrl(request?: ApiRequestContext): Promise { return getApiBaseUrl(request || (usesCurrentApiOrigin() ? {headers: await headers()} : undefined)); } export async function upstream(path: string, options: {method?: string; body?: unknown; token?: string; device?: string; userAgent?: string; clientIp?: string; timeoutMs?:number; request?:ApiRequestContext} = {}): Promise> { const url=new URL(`/api/app/${path}`,await getServerApiBaseUrl(options.request)); const policy=publicCachePolicy(url.pathname.slice('/api/app/'.length)); if(policy&&(!options.method||options.method==='GET')&&!options.token&&options.body===undefined){ url.searchParams.sort(); return publicDataCache.read(url.href,policy.ttl,()=>requestUpstream(url,options),{stale:policy.stale,accept:result=>result.code===200&&!!result.data&&typeof result.data==='object'&&!Array.isArray(result.data)}); } return requestUpstream(url,options); } async function requestUpstream(url:URL,options:Parameters[1]={}):Promise> { const device = (options.device || "server-public").replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 100); const rawUA = options.userAgent || "91-drama-web/1.0"; const sys = /Android/i.test(rawUA) ? "h5_android" : /iPhone|iPad|iPod/i.test(rawUA) ? "h5_ios" : "h5_pc"; const headers: Record = { Accept: "application/json", "User-Agent": rawUA, [UPSTREAM_HOP_HEADER]: '1', "X-User-Agent": `DevID=${device};DevType=OTHER;SysType=${sys};Ver=1.0.0;BuildID=web`, }; if (options.token) headers.Authorization = options.token; if (options.clientIp) headers['X-Real-IP'] = options.clientIp; const multipart = options.body instanceof FormData; if (options.body && !multipart) headers["Content-Type"] = "application/json"; let response: Response; try { response = await fetch(url, { method: options.method || "GET", headers, cache: "no-store", redirect: "error", body: options.body ? multipart ? options.body as FormData : JSON.stringify(options.body) : undefined, signal: AbortSignal.timeout(options.timeoutMs||20000), }); } catch (error) { const timeout = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError"); throw new ApiError(timeout ? "接口请求超时,请稍后重试" : "接口连接失败,请稍后重试", timeout ? 504 : 502); } if (!response.ok) { if (response.status === 502 && response.headers.get(UPSTREAM_HOP_HEADER) === '1') throw new ApiError('上游请求返回本站,请检查 Nginx 的 /api/app/ 后端转发配置',502); throw new ApiError(`接口暂时不可用(${response.status})`, response.status); } try { return decodeEnvelope(await response.json()); } catch (error) { if (error instanceof ApiError) throw error; throw new ApiError("接口返回数据无法解析", 502); } } export const getHome = cache(async (): Promise<{data: HomeData; error?: string}> => { try { const result = await upstream("playlet/home"); if (result.code !== 200) throw new ApiError(result.tip || result.msg || "首页加载失败",result.code); if (!result.data || typeof result.data !== 'object' || Array.isArray(result.data)) throw new ApiError('首页返回数据无效',502); return {data: result.data}; } catch (error) { return {data: {}, error: error instanceof Error ? error.message : "首页加载失败"}; } }); export const getHeaderNavigation=cache(async ():Promise<{data:HeaderNavigation;error?:boolean}>=>{ try{ const response=await upstream('navigation/header',{timeoutMs:5000}); if(response.code!==200||!response.data||typeof response.data!=='object')throw new Error('导航加载失败'); return {data:response.data}; }catch{return {data:{categoryList:[]},error:true};} });