Files
web_hgcg_h5/src/lib/api/server.ts
T
2026-10-29 16:25:57 +07:00

92 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<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; 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){
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;
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};}
});