import { NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { resolveThreeDModelAliasId } from "@/lib/model3d/model-alias-config"; import { getThreeDModelAliasConfigForRuntime } from "@/lib/model3d/model-alias-config-server"; import { uploadFile } from "@/lib/storage/service"; import { WorldLabsApiError, worldLabsRequest } from "@/lib/worldlabs/client"; import { getWorldLabsProviderConfig } from "@/lib/worldlabs/server"; import type { WorldLabsOperation, WorldLabsWorld, WorldLabsWorldAssets, } from "@/lib/worldlabs/types"; export const runtime = "nodejs"; export const maxDuration = 60; type ArchiveCacheItem = { sourceUrl: string; archivedUrl: string; }; function getArchiveCache() { const globalState = globalThis as any; if (!globalState.__worldLabsAssetArchiveCache) { globalState.__worldLabsAssetArchiveCache = new Map(); } return globalState.__worldLabsAssetArchiveCache as Map; } function getArchiveInflight() { const globalState = globalThis as any; if (!globalState.__worldLabsAssetArchiveInflight) { globalState.__worldLabsAssetArchiveInflight = new Map>(); } return globalState.__worldLabsAssetArchiveInflight as Map>; } function resolveRemoteFileExtension(url: string, fallback: string) { try { const parsed = new URL(url); const pathname = parsed.pathname.toLowerCase(); const ext = pathname.split(".").pop() || ""; return ext || fallback; } catch { return fallback; } } async function archiveRemoteAssetIfNeeded(params: { cacheKey: string; sourceUrl?: string | null; filenamePrefix: string; fallbackExtension: string; }) { const sourceUrl = String(params.sourceUrl || "").trim(); if (!sourceUrl) return null; const cache = getArchiveCache(); const cached = cache.get(params.cacheKey); if (cached && cached.sourceUrl === sourceUrl) { return cached.archivedUrl; } const inflight = getArchiveInflight(); const existingTask = inflight.get(params.cacheKey); if (existingTask) { const result = await existingTask; return result || sourceUrl; } const archiveTask = (async () => { try { const upstream = await fetch(sourceUrl, { method: "GET", headers: { "User-Agent": "AI-Chatbot-WorldLabs-Archiver/1.0", }, cache: "no-store", }); if (!upstream.ok) { throw new Error(`下载 World Labs 资源失败(HTTP ${upstream.status})`); } const assetBuffer = await upstream.arrayBuffer(); if (!assetBuffer || assetBuffer.byteLength <= 0) { throw new Error("World Labs 资源文件为空"); } const ext = resolveRemoteFileExtension(sourceUrl, params.fallbackExtension); const filename = `${params.filenamePrefix}.${ext}`; const uploaded = await uploadFile(filename, assetBuffer); cache.set(params.cacheKey, { sourceUrl, archivedUrl: uploaded.url, }); return uploaded.url; } catch (error) { console.error("[WorldLabsArchive] failed", { cacheKey: params.cacheKey, sourceUrl, error: error instanceof Error ? error.message : String(error), }); return null; } finally { inflight.delete(params.cacheKey); } })(); inflight.set(params.cacheKey, archiveTask); const archivedUrl = await archiveTask; return archivedUrl || sourceUrl; } async function archiveRemoteAssetMapIfNeeded(params: { cacheKeyPrefix: string; sourceMap?: Record | null; filenamePrefix: string; fallbackExtension: string; }) { const entries = Object.entries(params.sourceMap || {}).filter( (entry): entry is [string, string] => Boolean(entry[0]) && typeof entry[1] === "string" && entry[1].trim().length > 0 ); if (entries.length === 0) return null; const archivedEntries = await Promise.all( entries.map(async ([key, value]) => { const archivedUrl = await archiveRemoteAssetIfNeeded({ cacheKey: `${params.cacheKeyPrefix}:${key}`, sourceUrl: value, filenamePrefix: `${params.filenamePrefix}-${key}`, fallbackExtension: params.fallbackExtension, }); return [key, archivedUrl || value] as const; }) ); return Object.fromEntries(archivedEntries); } function getStringValue(...values: unknown[]) { for (const value of values) { if (typeof value === "string" && value.trim()) { return value.trim(); } } return null; } function getObjectValue(value: unknown) { return value && typeof value === "object" ? (value as Record) : null; } function extractWorldId(operation: WorldLabsOperation) { const response = getObjectValue(operation.response); const metadata = getObjectValue(operation.metadata); return getStringValue( response?.world_id, response?.worldId, metadata?.world_id, metadata?.worldId ); } function extractWorldFromOperation(operation: WorldLabsOperation) { const response = operation.response; if (response && typeof response === "object" && "world_marble_url" in (response as Record)) { return response as WorldLabsWorld; } return null; } function buildAssetPayload( world: WorldLabsWorld, worldModelAliases: Awaited< ReturnType >["worldModels"], assets?: WorldLabsWorldAssets | null, ) { return { worldId: getStringValue(world.world_id), worldUrl: getStringValue(world.world_marble_url), displayName: getStringValue(world.display_name), model: resolveThreeDModelAliasId(getStringValue(world.model), worldModelAliases), caption: getStringValue(assets?.caption), thumbnailUrl: getStringValue(assets?.thumbnail_url), panoUrl: getStringValue(assets?.imagery?.pano_url), meshUrl: getStringValue(assets?.mesh?.collider_mesh_url), spzUrls: assets?.splats?.spz_urls && typeof assets.splats.spz_urls === "object" ? assets.splats.spz_urls : null, semanticsMetadata: assets?.splats?.semantics_metadata && typeof assets.splats.semantics_metadata === "object" ? assets.splats.semantics_metadata : null, }; } interface RouteParams { params: Promise<{ taskId: string }>; } export async function GET(request: Request, { params }: RouteParams) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); } const { taskId } = await params; const normalizedTaskId = String(taskId || "").trim(); if (!normalizedTaskId) { return NextResponse.json({ success: false, error: "缺少 taskId" }, { status: 400 }); } const providerId = new URL(request.url).searchParams.get("providerId"); const config = await getWorldLabsProviderConfig(providerId); const modelAliasConfig = await getThreeDModelAliasConfigForRuntime(); const operation = await worldLabsRequest(config, { method: "GET", path: `/marble/v1/operations/${encodeURIComponent(normalizedTaskId)}`, }); if (!operation.done) { return NextResponse.json({ success: true, data: { taskId: normalizedTaskId, type: "world", status: "running", progress: null, output: operation.response ?? null, assetPayload: { providerId: config.providerId || null, providerName: config.providerName || null, operationMetadata: operation.metadata ?? null, }, }, }); } if (operation.error?.message) { return NextResponse.json({ success: true, data: { taskId: normalizedTaskId, type: "world", status: "failed", progress: null, errorMessage: operation.error.message, output: operation.response ?? null, }, }); } let world = extractWorldFromOperation(operation); const worldId = extractWorldId(operation); if (!world && worldId) { world = await worldLabsRequest(config, { method: "GET", path: `/marble/v1/worlds/${encodeURIComponent(worldId)}`, }); } if (!world) { throw new Error("World Labs 任务已完成,但未返回 world 数据"); } const assets = world.assets || null; const rawMeshUrl = getStringValue(assets?.mesh?.collider_mesh_url); const rawThumbnailUrl = getStringValue(assets?.thumbnail_url); const rawPanoUrl = getStringValue(assets?.imagery?.pano_url); const stableModelUrl = await archiveRemoteAssetIfNeeded({ cacheKey: `worldlabs:${normalizedTaskId}:mesh`, sourceUrl: rawMeshUrl, filenamePrefix: `worldlabs-mesh-${normalizedTaskId}`, fallbackExtension: "glb", }); const stableThumbnailUrl = await archiveRemoteAssetIfNeeded({ cacheKey: `worldlabs:${normalizedTaskId}:thumb`, sourceUrl: rawThumbnailUrl, filenamePrefix: `worldlabs-thumb-${normalizedTaskId}`, fallbackExtension: "jpg", }); const stablePanoUrl = await archiveRemoteAssetIfNeeded({ cacheKey: `worldlabs:${normalizedTaskId}:pano`, sourceUrl: rawPanoUrl, filenamePrefix: `worldlabs-pano-${normalizedTaskId}`, fallbackExtension: "jpg", }); const stableSpzUrls = await archiveRemoteAssetMapIfNeeded({ cacheKeyPrefix: `worldlabs:${normalizedTaskId}:spz`, sourceMap: assets?.splats?.spz_urls && typeof assets.splats.spz_urls === "object" ? assets.splats.spz_urls : null, filenamePrefix: `worldlabs-splat-${normalizedTaskId}`, fallbackExtension: "spz", }); const assetPayload = { providerId: config.providerId || null, providerName: config.providerName || null, ...buildAssetPayload(world, modelAliasConfig.worldModels, assets), }; return NextResponse.json({ success: true, data: { taskId: normalizedTaskId, type: "world", status: "success", progress: 100, output: operation.response ?? null, modelUrl: stableModelUrl, previewImageUrl: stableThumbnailUrl || stablePanoUrl || null, worldId: assetPayload.worldId, worldUrl: assetPayload.worldUrl, panoUrl: stablePanoUrl, caption: assetPayload.caption, assetPayload: { ...assetPayload, thumbnailUrl: stableThumbnailUrl || assetPayload.thumbnailUrl, panoUrl: stablePanoUrl || assetPayload.panoUrl, meshUrl: stableModelUrl || assetPayload.meshUrl, spzUrls: stableSpzUrls || assetPayload.spzUrls, }, }, }); } 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 } ); } }