105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
import { NextResponse } from "next/server";
|
||
import { auth } from "@/app/(auth)/auth";
|
||
|
||
export const runtime = "nodejs";
|
||
export const maxDuration = 60;
|
||
|
||
function isAllowedModelHost(hostname: string) {
|
||
const normalized = hostname.toLowerCase();
|
||
return (
|
||
normalized === "tripo3d.com" ||
|
||
normalized.endsWith(".tripo3d.com") ||
|
||
normalized.includes("worldlabs") ||
|
||
normalized.includes("marble") ||
|
||
normalized.includes("s3.1080p.hk") ||
|
||
normalized.includes("img.yydd.ai") ||
|
||
normalized.includes("blob.vercel-storage.com") ||
|
||
normalized.includes("aliyuncs.com") ||
|
||
normalized.includes("amazonaws.com") ||
|
||
normalized.includes("googleapis.com") ||
|
||
normalized.includes("googleusercontent.com") ||
|
||
normalized.includes("cloudfront.net")
|
||
);
|
||
}
|
||
|
||
export async function GET(request: Request) {
|
||
try {
|
||
const session = await auth();
|
||
if (!session?.user?.id) {
|
||
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
|
||
}
|
||
|
||
const { searchParams } = new URL(request.url);
|
||
const rawUrl = String(searchParams.get("url") || "").trim();
|
||
if (!rawUrl) {
|
||
return NextResponse.json({ success: false, error: "缺少 url 参数" }, { status: 400 });
|
||
}
|
||
|
||
let targetUrl: URL;
|
||
try {
|
||
targetUrl = new URL(rawUrl);
|
||
} catch {
|
||
return NextResponse.json({ success: false, error: "url 参数无效" }, { status: 400 });
|
||
}
|
||
|
||
if (targetUrl.protocol !== "https:") {
|
||
return NextResponse.json({ success: false, error: "仅支持 https 模型地址" }, { status: 400 });
|
||
}
|
||
|
||
if (!isAllowedModelHost(targetUrl.hostname)) {
|
||
return NextResponse.json({ success: false, error: "不允许的模型地址域名" }, { status: 403 });
|
||
}
|
||
|
||
const forwardedHeaders = new Headers();
|
||
const range = request.headers.get("range");
|
||
if (range) {
|
||
forwardedHeaders.set("range", range);
|
||
}
|
||
|
||
const upstream = await fetch(targetUrl.toString(), {
|
||
method: "GET",
|
||
headers: forwardedHeaders,
|
||
cache: "no-store",
|
||
});
|
||
|
||
if (!upstream.ok && upstream.status !== 206) {
|
||
const upstreamText = await upstream.text().catch(() => "");
|
||
return NextResponse.json(
|
||
{
|
||
success: false,
|
||
error: upstreamText || `模型拉取失败(HTTP ${upstream.status})`,
|
||
},
|
||
{ status: upstream.status || 502 }
|
||
);
|
||
}
|
||
|
||
const responseHeaders = new Headers();
|
||
const contentType = upstream.headers.get("content-type") || "application/octet-stream";
|
||
responseHeaders.set("content-type", contentType);
|
||
|
||
const contentLength = upstream.headers.get("content-length");
|
||
if (contentLength) responseHeaders.set("content-length", contentLength);
|
||
|
||
const contentRange = upstream.headers.get("content-range");
|
||
if (contentRange) responseHeaders.set("content-range", contentRange);
|
||
|
||
const acceptRanges = upstream.headers.get("accept-ranges");
|
||
if (acceptRanges) responseHeaders.set("accept-ranges", acceptRanges);
|
||
|
||
responseHeaders.set("cache-control", "private, no-store");
|
||
|
||
return new NextResponse(upstream.body, {
|
||
status: upstream.status,
|
||
headers: responseHeaders,
|
||
});
|
||
} catch (error) {
|
||
return NextResponse.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : "代理拉取 3D 模型失败",
|
||
},
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|