134 lines
3.5 KiB
TypeScript
134 lines
3.5 KiB
TypeScript
import { auth } from "@/app/(auth)/auth";
|
||
import { getSystemSetting } from "@/lib/db/queries";
|
||
|
||
const ALLOWED_FAL_HOSTS = new Set(["rest.alpha.fal.ai", "rest.fal.ai"]);
|
||
|
||
type FalRealtimeConfig = {
|
||
enabled?: boolean;
|
||
falApiKey?: string;
|
||
apiKey?: string;
|
||
};
|
||
|
||
type AngleEditConfig = {
|
||
falApiKey?: string;
|
||
};
|
||
|
||
async function getRealtimeCanvasConfig() {
|
||
const [realtimeSetting, angleEditSetting] = await Promise.all([
|
||
getSystemSetting({ key: "realtime_canvas_config" }).catch(() => null),
|
||
getSystemSetting({ key: "angle_edit_config" }).catch(() => null),
|
||
]);
|
||
|
||
const realtimeConfig = (realtimeSetting?.value || {}) as FalRealtimeConfig;
|
||
const angleEditConfig = (angleEditSetting?.value || {}) as AngleEditConfig;
|
||
const fallbackKey = String(
|
||
angleEditConfig.falApiKey || process.env.FAL_KEY || process.env.FAL_API_KEY || ""
|
||
).trim();
|
||
|
||
return {
|
||
...realtimeConfig,
|
||
falApiKey:
|
||
String(realtimeConfig.falApiKey || realtimeConfig.apiKey || "").trim() || fallbackKey,
|
||
} as FalRealtimeConfig;
|
||
}
|
||
|
||
async function handleProxy(request: Request) {
|
||
const session = await auth();
|
||
if (!session?.user?.id) {
|
||
return Response.json({ code: "UNAUTHORIZED", message: "请先登录" }, { status: 401 });
|
||
}
|
||
|
||
const targetUrl = request.headers.get("x-fal-target-url");
|
||
if (!targetUrl) {
|
||
return Response.json(
|
||
{ code: "INVALID_TARGET", message: "缺少 x-fal-target-url 请求头" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
let parsedUrl: URL;
|
||
try {
|
||
parsedUrl = new URL(targetUrl);
|
||
} catch {
|
||
return Response.json(
|
||
{ code: "INVALID_TARGET", message: "x-fal-target-url 不是合法 URL" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
if (!ALLOWED_FAL_HOSTS.has(parsedUrl.host)) {
|
||
return Response.json(
|
||
{ code: "TARGET_NOT_ALLOWED", message: "不允许代理该目标地址" },
|
||
{ status: 403 }
|
||
);
|
||
}
|
||
|
||
const realtimeConfig = await getRealtimeCanvasConfig();
|
||
if (realtimeConfig.enabled === false) {
|
||
return Response.json(
|
||
{
|
||
code: "FEATURE_DISABLED",
|
||
message: "实时画布服务未启用(请在本地AI设置中开启)",
|
||
},
|
||
{ status: 403 }
|
||
);
|
||
}
|
||
|
||
const falApiKey = realtimeConfig.falApiKey?.trim() || "";
|
||
if (!falApiKey) {
|
||
return Response.json(
|
||
{
|
||
code: "FAL_KEY_MISSING",
|
||
message: "未配置 FAL API Key(请在本地AI设置的实时画布配置中填写)",
|
||
},
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
|
||
let body: ArrayBuffer | undefined;
|
||
if (request.method !== "GET" && request.method !== "HEAD") {
|
||
body = await request.arrayBuffer();
|
||
}
|
||
|
||
const headers = new Headers();
|
||
headers.set("Authorization", `Key ${falApiKey}`);
|
||
headers.set("Accept", request.headers.get("accept") || "application/json");
|
||
const contentType = request.headers.get("content-type");
|
||
if (contentType) {
|
||
headers.set("Content-Type", contentType);
|
||
}
|
||
|
||
const upstreamResponse = await fetch(parsedUrl.toString(), {
|
||
method: request.method,
|
||
headers,
|
||
body,
|
||
cache: "no-store",
|
||
});
|
||
|
||
return new Response(upstreamResponse.body, {
|
||
status: upstreamResponse.status,
|
||
statusText: upstreamResponse.statusText,
|
||
headers: upstreamResponse.headers,
|
||
});
|
||
}
|
||
|
||
export async function GET(request: Request) {
|
||
return handleProxy(request);
|
||
}
|
||
|
||
export async function POST(request: Request) {
|
||
return handleProxy(request);
|
||
}
|
||
|
||
export async function PUT(request: Request) {
|
||
return handleProxy(request);
|
||
}
|
||
|
||
export async function PATCH(request: Request) {
|
||
return handleProxy(request);
|
||
}
|
||
|
||
export async function DELETE(request: Request) {
|
||
return handleProxy(request);
|
||
}
|