From e05b42942d61c88204ca0f17ab264cba58f8e735 Mon Sep 17 00:00:00 2001 From: 134 Date: Wed, 19 Aug 2026 13:48:07 +0800 Subject: [PATCH] fix(world): fallback status polling from failed external task result --- app/api/canvas/world/tasks/[taskId]/route.ts | 335 ++++++++++--------- 1 file changed, 179 insertions(+), 156 deletions(-) diff --git a/app/api/canvas/world/tasks/[taskId]/route.ts b/app/api/canvas/world/tasks/[taskId]/route.ts index e2c99b272..978603df3 100644 --- a/app/api/canvas/world/tasks/[taskId]/route.ts +++ b/app/api/canvas/world/tasks/[taskId]/route.ts @@ -730,185 +730,208 @@ export async function GET(request: Request, { params }: RouteParams) { }); providerForLog = config.providerId || null; const modelAliasConfig = await getThreeDModelAliasConfigForRuntime(); - const persistedAssetPayload = + let persistedAssetPayload = getObjectValue(persistedWorldAsset?.assetPayload) || {}; - const shouldUseExternalTaskResultApi = + let shouldUseExternalTaskResultApi = isWorldLabsExternalTaskApi(config.baseUrl) && persistedAssetPayload.externalTaskApi !== false && persistedAssetPayload.marbleApi !== true; if (shouldUseExternalTaskResultApi) { - const externalPayload = await worldLabsRequest< - WorldLabsExternalTaskResponse - >(config, { - method: "GET", - path: `/api/external/task/result?taskId=${encodeURIComponent(normalizedTaskId)}`, - timeoutMs: resolveWorldLabsStatusTimeoutMs(), - }); - const externalData = - unwrapExternalTaskData(externalPayload); - const externalStatusText = getStringValue(externalData.status) || "running"; - const externalStatus = normalizeExternalTaskStatus( - externalStatusText, - externalData.done === true - ); - const externalAssetPayloadBase = { - ...persistedAssetPayload, - providerId: config.providerId || null, - providerName: config.providerName || null, - externalTaskApi: true, - externalTaskStatus: externalStatusText, - requestId: getStringValue(externalPayload.requestId, externalPayload.request_id), - }; - - if (externalStatus === "running") { - return NextResponse.json({ - success: true, - data: { - taskId: normalizedTaskId, - type: "world", - status: "running", - progress: null, - output: externalData, - assetPayload: externalAssetPayloadBase, - }, + try { + const externalPayload = await worldLabsRequest< + WorldLabsExternalTaskResponse + >(config, { + method: "GET", + path: `/api/external/task/result?taskId=${encodeURIComponent(normalizedTaskId)}`, + timeoutMs: resolveWorldLabsStatusTimeoutMs(), }); - } - - if (externalStatus === "failed") { - const errorMessage = extractExternalTaskErrorMessage( - externalData, - externalPayload + const externalData = + unwrapExternalTaskData(externalPayload); + const externalStatusText = getStringValue(externalData.status) || "running"; + const externalStatus = normalizeExternalTaskStatus( + externalStatusText, + externalData.done === true ); + const externalAssetPayloadBase = { + ...persistedAssetPayload, + providerId: config.providerId || null, + providerName: config.providerName || null, + externalTaskApi: true, + externalTaskStatus: externalStatusText, + requestId: getStringValue(externalPayload.requestId, externalPayload.request_id), + }; + + if (externalStatus === "running") { + return NextResponse.json({ + success: true, + data: { + taskId: normalizedTaskId, + type: "world", + status: "running", + progress: null, + output: externalData, + assetPayload: externalAssetPayloadBase, + }, + }); + } + + if (externalStatus === "failed") { + const errorMessage = extractExternalTaskErrorMessage( + externalData, + externalPayload + ); + await updatePersistedWorldAsset({ + id: persistedWorldAsset?.id, + status: "failed", + progress: null, + error: errorMessage, + assetPayload: externalAssetPayloadBase, + }); + logger.warn("external task failed status", { + elapsedMs: Date.now() - startedAt, + taskIdLength: normalizedTaskId.length, + providerId: config.providerId || null, + error: errorMessage, + }); + return NextResponse.json({ + success: true, + data: { + taskId: normalizedTaskId, + type: "world", + status: "failed", + progress: null, + errorMessage, + output: externalData, + assetPayload: externalAssetPayloadBase, + }, + }); + } + const extractedAssetPayload = extractExternalWorldAssetPayload(externalData); + const rawMeshUrl = extractedAssetPayload.meshUrl; + const rawThumbnailUrl = extractedAssetPayload.thumbnailUrl; + const rawPanoUrl = extractedAssetPayload.panoUrl; + const stableWorldUrl: string | null = null; + const stableModelUrl = await archiveRemoteAssetIfNeeded({ + cacheKey: `worldlabs:${normalizedTaskId}:mesh`, + sourceUrl: rawMeshUrl, + filenamePrefix: `worldlabs-mesh-${normalizedTaskId}`, + fallbackExtension: "glb", + validateAs: "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: extractedAssetPayload.spzUrls, + filenamePrefix: `worldlabs-splat-${normalizedTaskId}`, + fallbackExtension: "spz", + validateAs: "spz", + }); + const assetPayload = { + ...externalAssetPayloadBase, + worldId: extractedAssetPayload.worldId, + worldUrl: stableWorldUrl, + displayName: extractedAssetPayload.displayName, + model: resolveThreeDModelAliasId( + extractedAssetPayload.model, + modelAliasConfig.worldModels + ), + caption: extractedAssetPayload.caption, + thumbnailUrl: stableThumbnailUrl || extractedAssetPayload.thumbnailUrl, + panoUrl: stablePanoUrl || extractedAssetPayload.panoUrl, + meshUrl: stableModelUrl, + spzUrls: stableSpzUrls, + semanticsMetadata: extractedAssetPayload.semanticsMetadata, + storageArchivedAt: new Date().toISOString(), + sourceWorldUrl: extractedAssetPayload.worldUrl || null, + sourceMeshUrl: + rawMeshUrl && stableModelUrl && rawMeshUrl !== stableModelUrl + ? rawMeshUrl + : null, + sourceThumbnailUrl: + rawThumbnailUrl && stableThumbnailUrl && rawThumbnailUrl !== stableThumbnailUrl + ? rawThumbnailUrl + : null, + sourcePanoUrl: + rawPanoUrl && stablePanoUrl && rawPanoUrl !== stablePanoUrl + ? rawPanoUrl + : null, + sourceSpzUrls: + extractedAssetPayload.spzUrls && + stableSpzUrls !== extractedAssetPayload.spzUrls + ? extractedAssetPayload.spzUrls + : null, + }; + await updatePersistedWorldAsset({ id: persistedWorldAsset?.id, - status: "failed", - progress: null, - error: errorMessage, - assetPayload: externalAssetPayloadBase, + status: "success", + progress: 100, + modelUrl: stableModelUrl, + previewImageUrl: stableThumbnailUrl || stablePanoUrl || null, + error: null, + assetPayload, }); - logger.warn("external task failed status", { + + logger.info("external task success status", { elapsedMs: Date.now() - startedAt, taskIdLength: normalizedTaskId.length, providerId: config.providerId || null, - error: errorMessage, + hasMesh: Boolean(stableModelUrl), + hasPano: Boolean(stablePanoUrl), + hasThumbnail: Boolean(stableThumbnailUrl), + hasSplat: Boolean(stableSpzUrls && Object.keys(stableSpzUrls).length > 0), }); + return NextResponse.json({ success: true, data: { taskId: normalizedTaskId, type: "world", - status: "failed", - progress: null, - errorMessage, + status: "success", + progress: 100, output: externalData, - assetPayload: externalAssetPayloadBase, + modelUrl: stableModelUrl, + previewImageUrl: stableThumbnailUrl || stablePanoUrl || null, + worldId: assetPayload.worldId, + worldUrl: stableWorldUrl, + panoUrl: stablePanoUrl, + caption: assetPayload.caption, + assetPayload, }, }); + } catch (error) { + const fallbackAssetPayload = { + ...persistedAssetPayload, + providerId: config.providerId || null, + providerName: config.providerName || null, + marbleApi: true, + externalTaskApi: false, + }; + shouldUseExternalTaskResultApi = false; + persistedAssetPayload = fallbackAssetPayload; + await updatePersistedWorldAsset({ + id: persistedWorldAsset?.id, + status: "running", + assetPayload: fallbackAssetPayload, + }).catch(() => {}); + logger.warn("external task result failed, fallback to marble status", { + elapsedMs: Date.now() - startedAt, + taskIdLength: normalizedTaskId.length, + providerId: config.providerId || null, + error: error instanceof Error ? error.message : String(error), + }); } - - const extractedAssetPayload = extractExternalWorldAssetPayload(externalData); - const rawMeshUrl = extractedAssetPayload.meshUrl; - const rawThumbnailUrl = extractedAssetPayload.thumbnailUrl; - const rawPanoUrl = extractedAssetPayload.panoUrl; - const stableWorldUrl: string | null = null; - const stableModelUrl = await archiveRemoteAssetIfNeeded({ - cacheKey: `worldlabs:${normalizedTaskId}:mesh`, - sourceUrl: rawMeshUrl, - filenamePrefix: `worldlabs-mesh-${normalizedTaskId}`, - fallbackExtension: "glb", - validateAs: "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: extractedAssetPayload.spzUrls, - filenamePrefix: `worldlabs-splat-${normalizedTaskId}`, - fallbackExtension: "spz", - validateAs: "spz", - }); - const assetPayload = { - ...externalAssetPayloadBase, - worldId: extractedAssetPayload.worldId, - worldUrl: stableWorldUrl, - displayName: extractedAssetPayload.displayName, - model: resolveThreeDModelAliasId( - extractedAssetPayload.model, - modelAliasConfig.worldModels - ), - caption: extractedAssetPayload.caption, - thumbnailUrl: stableThumbnailUrl || extractedAssetPayload.thumbnailUrl, - panoUrl: stablePanoUrl || extractedAssetPayload.panoUrl, - meshUrl: stableModelUrl, - spzUrls: stableSpzUrls, - semanticsMetadata: extractedAssetPayload.semanticsMetadata, - storageArchivedAt: new Date().toISOString(), - sourceWorldUrl: extractedAssetPayload.worldUrl || null, - sourceMeshUrl: - rawMeshUrl && stableModelUrl && rawMeshUrl !== stableModelUrl - ? rawMeshUrl - : null, - sourceThumbnailUrl: - rawThumbnailUrl && stableThumbnailUrl && rawThumbnailUrl !== stableThumbnailUrl - ? rawThumbnailUrl - : null, - sourcePanoUrl: - rawPanoUrl && stablePanoUrl && rawPanoUrl !== stablePanoUrl - ? rawPanoUrl - : null, - sourceSpzUrls: - extractedAssetPayload.spzUrls && stableSpzUrls !== extractedAssetPayload.spzUrls - ? extractedAssetPayload.spzUrls - : null, - }; - - await updatePersistedWorldAsset({ - id: persistedWorldAsset?.id, - status: "success", - progress: 100, - modelUrl: stableModelUrl, - previewImageUrl: stableThumbnailUrl || stablePanoUrl || null, - error: null, - assetPayload, - }); - - logger.info("external task success status", { - elapsedMs: Date.now() - startedAt, - taskIdLength: normalizedTaskId.length, - providerId: config.providerId || null, - hasMesh: Boolean(stableModelUrl), - hasPano: Boolean(stablePanoUrl), - hasThumbnail: Boolean(stableThumbnailUrl), - hasSplat: Boolean(stableSpzUrls && Object.keys(stableSpzUrls).length > 0), - }); - - return NextResponse.json({ - success: true, - data: { - taskId: normalizedTaskId, - type: "world", - status: "success", - progress: 100, - output: externalData, - modelUrl: stableModelUrl, - previewImageUrl: stableThumbnailUrl || stablePanoUrl || null, - worldId: assetPayload.worldId, - worldUrl: stableWorldUrl, - panoUrl: stablePanoUrl, - caption: assetPayload.caption, - assetPayload, - }, - }); } const operation = await worldLabsRequest(config, {