@@ -0,0 +1,96 @@
|
||||
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",
|
||||
"recharge/package/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", "playlet/collect/list", "playlet/chapter/collect/list",
|
||||
"playlet/history/list", "playlet/like/list", "playlet/bought/list", "user/center/data", "coin/orders", "coin/trans/list",
|
||||
"vip/orders", "comment/list",
|
||||
];
|
||||
const POST_PATHS = [
|
||||
"auth/guest", "auth/login", "auth/register", "auth/resetPassword", "auth/profile",
|
||||
"playlet/collect/set", "playlet/like/set", "playlet/chapter/collect/set", "playlet/chapter/like/set",
|
||||
"playlet/chapter/unlock", "playlet/chapter/watch/report", "playlet/forward/heat",
|
||||
"playlet/history/clear", "playlet/history/delete", "comment/send", "comment/like/set",
|
||||
"coin/buy", "vip/pay", "feedback/add", "upload/image",
|
||||
];
|
||||
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<string> {
|
||||
return getApiBaseUrl(request || (usesCurrentApiOrigin() ? {headers: await headers()} : undefined));
|
||||
}
|
||||
|
||||
export async function upstream<T>(path: string, options: {method?: string; body?: unknown; token?: string; device?: string; userAgent?: string; clientIp?: string; tracking?:Record<string,string>; timeoutMs?:number; request?:ApiRequestContext} = {}): Promise<Envelope<T>> {
|
||||
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&&!Object.keys(options.tracking||{}).length){
|
||||
url.searchParams.sort();
|
||||
return publicDataCache.read(url.href,policy.ttl,()=>requestUpstream<T>(url,options),{stale:policy.stale,accept:result=>result.code===200&&!!result.data&&typeof result.data==='object'&&!Array.isArray(result.data)});
|
||||
}
|
||||
return requestUpstream<T>(url,options);
|
||||
}
|
||||
|
||||
async function requestUpstream<T>(url:URL,options:Parameters<typeof upstream>[1]={}):Promise<Envelope<T>> {
|
||||
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<string,string> = {
|
||||
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;
|
||||
for(const key of ['X-SDK-Common','X-User-Agent','X-Tracker-Request-Id','X-Tracker-Event-Name','X-DC-Request-Context'])if(options.tracking?.[key])headers[key]=options.tracking[key];
|
||||
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<T>(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<HomeData>("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<HeaderNavigation>('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};}
|
||||
});
|
||||
Reference in New Issue
Block a user