2026-04-05 21:54:11 +08:00

250 lines
7.2 KiB
TypeScript

import { NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
import {
consumeUserPoints,
getPointsSystemConfig,
getUserById,
} from "@/lib/db/queries";
import { WorldLabsApiError, worldLabsRequest } from "@/lib/worldlabs/client";
import { getWorldLabsProviderConfig } from "@/lib/worldlabs/server";
import type { WorldLabsModel, WorldLabsOperation } from "@/lib/worldlabs/types";
export const runtime = "nodejs";
export const maxDuration = 60;
type WorldTaskMode = "text" | "image" | "multi-image" | "video";
type CreateWorldTaskRequest = {
mode?: WorldTaskMode;
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 } : {}),
};
}
export async function POST(request: Request) {
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;
if (!["text", "image", "multi-image", "video"].includes(mode)) {
return NextResponse.json({ success: false, error: "缺少有效的 mode" }, { status: 400 });
}
const pointsConfig = await getPointsSystemConfig();
const shouldChargePoints = shouldCharge3DPoints(mode);
const unitCost = Number.isFinite(Number(pointsConfig.creditCostPer3DWorld))
? Math.max(0, Math.round(Number(pointsConfig.creditCostPer3DWorld)))
: 0;
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 config = await getWorldLabsProviderConfig();
const displayName =
readOptionalTrimmedText(body.displayName) ||
readOptionalTrimmedText(body.prompt) ||
`World ${new Date().toLocaleString("zh-CN")}`;
const worldPrompt = buildWorldPrompt(body, mode);
const result = await worldLabsRequest<WorldLabsOperation>(config, {
method: "POST",
path: "/marble/v1/worlds:generate",
json: {
display_name: displayName,
model: readOptionalTrimmedText(body.model) || "marble-1.1",
permission: {
public: body.makePublic === true,
},
world_prompt: worldPrompt,
},
});
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",
note: "3D世界生成消耗积分",
});
} catch (error) {
console.error("[Points] Failed to consume World Labs 3D points:", error);
}
}
return NextResponse.json({
success: true,
data: {
taskId,
status: result.done ? "success" : "running",
raw: result,
},
});
} 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 }
);
}
}