import { NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { consumeUserPoints, getPointsSystemConfig, getUserById, } from "@/lib/db/queries"; import { resolveThreeDModelDisplayName, resolveThreeDModelRealId, } from "@/lib/model3d/model-alias-config"; import { getThreeDModelAliasConfigForRuntime } from "@/lib/model3d/model-alias-config-server"; import { createLogger } from "@/lib/observability/logger"; import { get3DWorldCreditCost } from "@/lib/points/config"; import { WorldLabsApiError, worldLabsRequest } from "@/lib/worldlabs/client"; import { disableWorldLabsProvider, getWorldLabsProviderConfig, getWorldLabsProviderConfigs, } from "@/lib/worldlabs/server"; import type { WorldLabsModel, WorldLabsOperation } from "@/lib/worldlabs/types"; export const runtime = "nodejs"; export const maxDuration = 60; const logger = createLogger("world-labs-task-create"); type WorldTaskMode = "text" | "image" | "multi-image" | "video"; type CreateWorldTaskRequest = { mode?: WorldTaskMode; providerId?: string; displayName?: string; prompt?: string; model?: WorldLabsModel | string; disableRecaption?: boolean; isPano?: boolean; makePublic?: boolean; reconstructImages?: boolean; mediaAssetId?: string; mediaAssets?: Array<{ mediaAssetId?: string; azimuth?: number | null; }>; }; function readOptionalTrimmedText(input: unknown) { const value = String(input || "").trim(); return value || null; } function shouldCharge3DPoints(mode: WorldTaskMode) { return mode === "text" || mode === "image" || mode === "multi-image" || mode === "video"; } function buildWorldPrompt(body: CreateWorldTaskRequest, mode: WorldTaskMode) { const textPrompt = readOptionalTrimmedText(body.prompt); if (mode === "text") { if (!textPrompt) { throw new Error("请输入世界描述"); } return { type: "text", text_prompt: textPrompt, ...(body.disableRecaption !== undefined ? { disable_recaption: body.disableRecaption } : {}), }; } if (mode === "image") { const mediaAssetId = readOptionalTrimmedText(body.mediaAssetId); if (!mediaAssetId) { throw new Error("图生世界缺少 mediaAssetId"); } return { type: "image", image_prompt: { source: "media_asset", media_asset_id: mediaAssetId, }, ...(textPrompt ? { text_prompt: textPrompt } : {}), ...(body.disableRecaption !== undefined ? { disable_recaption: body.disableRecaption } : {}), ...(body.isPano !== undefined ? { is_pano: body.isPano } : {}), }; } if (mode === "multi-image") { const mediaAssets = Array.isArray(body.mediaAssets) ? body.mediaAssets : []; const multiImagePrompt = mediaAssets .map((item) => { const mediaAssetId = readOptionalTrimmedText(item?.mediaAssetId); if (!mediaAssetId) return null; const azimuth = item?.azimuth === null || item?.azimuth === undefined ? null : Number.isFinite(Number(item.azimuth)) ? Number(item.azimuth) : null; return { ...(azimuth === null ? {} : { azimuth }), content: { source: "media_asset", media_asset_id: mediaAssetId, }, }; }) .filter(Boolean); if (multiImagePrompt.length < 2) { throw new Error("多图生成世界至少需要 2 张图片"); } return { type: "multi-image", multi_image_prompt: multiImagePrompt, ...(textPrompt ? { text_prompt: textPrompt } : {}), ...(body.disableRecaption !== undefined ? { disable_recaption: body.disableRecaption } : {}), ...(body.reconstructImages !== undefined ? { reconstruct_images: body.reconstructImages } : {}), }; } const mediaAssetId = readOptionalTrimmedText(body.mediaAssetId); if (!mediaAssetId) { throw new Error("视频生成世界缺少 mediaAssetId"); } return { type: "video", video_prompt: { source: "media_asset", media_asset_id: mediaAssetId, }, ...(textPrompt ? { text_prompt: textPrompt } : {}), }; } function isPermanentProviderError(error: unknown) { return ( error instanceof WorldLabsApiError && [401, 402, 403].includes(error.httpStatus) ); } export async function POST(request: Request) { const startedAt = Date.now(); let modeForLog: string | null = null; let modelForLog: string | null = null; let providerForLog: string | null = null; try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); } const body = (await request.json()) as CreateWorldTaskRequest; const mode = String(body.mode || "").trim() as WorldTaskMode; modeForLog = mode; if (!["text", "image", "multi-image", "video"].includes(mode)) { return NextResponse.json({ success: false, error: "缺少有效的 mode" }, { status: 400 }); } const modelAliasConfig = await getThreeDModelAliasConfigForRuntime(); const resolvedWorldModel = resolveThreeDModelRealId( readOptionalTrimmedText(body.model), modelAliasConfig.worldModels, ); const worldModelDisplayName = resolveThreeDModelDisplayName( resolvedWorldModel || "marble-1.1", modelAliasConfig.worldModels, resolvedWorldModel || "marble-1.1", ); modelForLog = resolvedWorldModel || "marble-1.1"; const pointsConfig = await getPointsSystemConfig(); const shouldChargePoints = shouldCharge3DPoints(mode); const unitCost = get3DWorldCreditCost( pointsConfig, resolvedWorldModel || "marble-1.1", ); if ( shouldChargePoints && pointsConfig.enabled && unitCost > 0 && pointsConfig.blockOnInsufficient !== false ) { const userData = await getUserById({ id: session.user.id }); if (!userData?.isHighVip) { const currentBalance = Math.max(0, Number(userData?.pointsBalance || 0)); if (currentBalance < unitCost) { return NextResponse.json( { success: false, error: "积分不足", code: "insufficient_points", message: `积分不足,当前余额 ${currentBalance},本次预计消耗 ${unitCost}。`, balance: currentBalance, expectedCost: unitCost, unitCost, }, { status: 402 } ); } } } const preferredProviderId = readOptionalTrimmedText(body.providerId); const displayName = readOptionalTrimmedText(body.displayName) || readOptionalTrimmedText(body.prompt) || `World ${new Date().toLocaleString("zh-CN")}`; const worldPrompt = buildWorldPrompt(body, mode); const candidates = preferredProviderId ? [await getWorldLabsProviderConfig(preferredProviderId)] : await getWorldLabsProviderConfigs(); if (candidates.length === 0) { throw new Error("未找到可用的 World Labs Key"); } let result: WorldLabsOperation | null = null; let usedProvider = null as Awaited< ReturnType > | null; let lastError: unknown = null; for (const candidate of candidates) { try { result = await worldLabsRequest(candidate, { method: "POST", path: "/marble/v1/worlds:generate", json: { display_name: displayName, model: resolvedWorldModel || "marble-1.1", permission: { public: body.makePublic === true, }, world_prompt: worldPrompt, }, }); usedProvider = candidate; providerForLog = candidate.providerId || null; break; } catch (error) { lastError = error; if (isPermanentProviderError(error)) { await disableWorldLabsProvider( candidate.providerId, error instanceof Error ? error.message : String(error) ); } if (preferredProviderId) { break; } } } if (!result || !usedProvider) { throw lastError instanceof Error ? lastError : new Error("创建 World Labs 世界任务失败"); } const taskId = String(result.operation_id || "").trim(); if (!taskId) { return NextResponse.json( { success: false, error: "World Labs 返回缺少 operation_id", raw: result, }, { status: 502 } ); } if (shouldChargePoints && pointsConfig.enabled && unitCost > 0) { try { await consumeUserPoints({ userId: session.user.id, amount: unitCost, reason: "world3d_generation", modelId: `worldlabs-3d:${resolvedWorldModel || "marble-1.1"}`, note: `3D世界生成消耗积分(${worldModelDisplayName})`, }); } catch (error) { logger.error("points consume failed", error, { userId: session.user.id, unitCost, mode, model: resolvedWorldModel || "marble-1.1", }); } } logger.info("task created", { elapsedMs: Date.now() - startedAt, taskIdLength: taskId.length, mode, model: resolvedWorldModel || "marble-1.1", providerId: usedProvider.providerId || null, status: result.done ? "success" : "running", unitCost, }); return NextResponse.json({ success: true, data: { taskId, status: result.done ? "success" : "running", assetPayload: { providerId: usedProvider.providerId || null, providerName: usedProvider.providerName || null, }, raw: result, }, }); } catch (error) { logger.error("task create failed", error, { elapsedMs: Date.now() - startedAt, mode: modeForLog, model: modelForLog, providerId: providerForLog, }); 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 } ); } }