250 lines
7.2 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|