Your Name 598cdfd736
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: async canvas image generation queue and stabilize runtime
2026-02-24 12:00:51 +08:00

134 lines
3.5 KiB
TypeScript
Raw Permalink 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 { 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);
}