Your Name 078a115cfd
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add worldlabs key pool import api
2026-04-05 23:08:04 +08:00

204 lines
6.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 { NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
import { WorldLabsApiError, worldLabsRequest } from "@/lib/worldlabs/client";
import {
disableWorldLabsProvider,
getWorldLabsProviderConfig,
getWorldLabsProviderConfigs,
} from "@/lib/worldlabs/server";
import type { WorldLabsPrepareUploadResponse } from "@/lib/worldlabs/types";
export const runtime = "nodejs";
export const maxDuration = 120;
const ALLOWED_IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/webp"]);
const ALLOWED_VIDEO_TYPES = new Set([
"video/mp4",
"video/quicktime",
"video/webm",
"video/x-m4v",
]);
const MAX_IMAGE_UPLOAD_SIZE = 20 * 1024 * 1024;
const MAX_VIDEO_UPLOAD_SIZE = 100 * 1024 * 1024;
function guessExtension(file: File) {
const fromName = String(file.name || "")
.split(".")
.pop()
?.trim()
.toLowerCase();
if (fromName) return fromName;
const byType: Record<string, string> = {
"image/png": "png",
"image/jpeg": "jpg",
"image/webp": "webp",
"video/mp4": "mp4",
"video/quicktime": "mov",
"video/webm": "webm",
"video/x-m4v": "m4v",
};
return byType[file.type] || "bin";
}
function normalizeMediaAssetId(data: WorldLabsPrepareUploadResponse) {
return String(data.media_asset?.media_asset_id || data.media_asset?.id || "").trim();
}
function isPermanentProviderError(error: unknown) {
return (
error instanceof WorldLabsApiError &&
[401, 402, 403].includes(error.httpStatus)
);
}
export async function POST(request: Request) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
}
const formData = await request.formData();
const file = formData.get("file");
if (!(file instanceof File)) {
return NextResponse.json({ success: false, error: "请上传文件" }, { status: 400 });
}
const requestedKind = String(formData.get("kind") || "").trim();
const isImage = requestedKind
? requestedKind === "image"
: ALLOWED_IMAGE_TYPES.has(file.type);
const isVideo = requestedKind
? requestedKind === "video"
: ALLOWED_VIDEO_TYPES.has(file.type);
if (!isImage && !isVideo) {
return NextResponse.json(
{ success: false, error: "仅支持 PNG/JPEG/WEBP 图片或 MP4/MOV/WEBM 视频" },
{ status: 400 }
);
}
if (file.size <= 0) {
return NextResponse.json({ success: false, error: "文件不能为空" }, { status: 400 });
}
const maxSize = isImage ? MAX_IMAGE_UPLOAD_SIZE : MAX_VIDEO_UPLOAD_SIZE;
if (file.size > maxSize) {
return NextResponse.json(
{
success: false,
error: isImage ? "图片大小不能超过 20MB" : "视频大小不能超过 100MB",
},
{ status: 400 }
);
}
const requestedProviderId = String(formData.get("providerId") || "").trim();
const candidates = requestedProviderId
? [await getWorldLabsProviderConfig(requestedProviderId)]
: await getWorldLabsProviderConfigs();
if (candidates.length === 0) {
throw new Error("未找到可用的 World Labs Key");
}
const extension = guessExtension(file);
let lastError: unknown = null;
for (const config of candidates) {
try {
const prepareUpload = await worldLabsRequest<WorldLabsPrepareUploadResponse>(
config,
{
method: "POST",
path: "/marble/v1/media-assets:prepare_upload",
json: {
file_name: file.name || `upload.${extension}`,
extension,
kind: isImage ? "image" : "video",
metadata: {
mime_type: file.type || null,
uploaded_from: "canvas_world_mode",
},
},
}
);
const mediaAssetId = normalizeMediaAssetId(prepareUpload);
const uploadUrl = String(prepareUpload.upload_info?.upload_url || "").trim();
if (!mediaAssetId || !uploadUrl) {
throw new Error("World Labs 返回的上传信息不完整");
}
const uploadHeaders = new Headers();
uploadHeaders.set("Content-Type", file.type || "application/octet-stream");
const requiredHeaders = prepareUpload.upload_info?.required_headers || {};
for (const [headerName, headerValue] of Object.entries(requiredHeaders)) {
if (!headerName || headerValue === undefined || headerValue === null) continue;
uploadHeaders.set(headerName, String(headerValue));
}
const uploadMethod = String(
prepareUpload.upload_info?.upload_method || "PUT"
).toUpperCase();
const uploadResponse = await fetch(uploadUrl, {
method: uploadMethod === "PUT" ? "PUT" : uploadMethod,
headers: uploadHeaders,
body: file,
cache: "no-store",
});
if (!uploadResponse.ok) {
const uploadError = await uploadResponse.text().catch(() => "");
throw new Error(
uploadError || `World Labs 文件上传失败(HTTP ${uploadResponse.status})`
);
}
return NextResponse.json({
success: true,
data: {
mediaAssetId,
mediaAsset: prepareUpload.media_asset,
uploadInfo: prepareUpload.upload_info,
providerId: config.providerId || null,
providerName: config.providerName || null,
},
});
} catch (error) {
lastError = error;
if (isPermanentProviderError(error)) {
await disableWorldLabsProvider(config.providerId, (error as Error).message);
}
if (requestedProviderId) {
break;
}
}
}
throw lastError instanceof Error
? lastError
: new Error("上传 World Labs 媒体失败");
} catch (error) {
if (error instanceof WorldLabsApiError) {
return NextResponse.json(
{
success: false,
error: error.message,
payload: error.payload,
},
{ status: error.httpStatus || 502 }
);
}
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "上传 World Labs 媒体失败",
},
{ status: 500 }
);
}
}