Your Name 48c6cd0f87
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: stabilize world 3d asset preview
2026-04-30 14:35:17 +08:00

105 lines
3.3 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 { 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 }
);
}
}