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 = { "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( 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 } ); } }