diff --git a/app/api/admin/settings/route.ts b/app/api/admin/settings/route.ts
index 9dcfe1bea..e2e683f5d 100644
--- a/app/api/admin/settings/route.ts
+++ b/app/api/admin/settings/route.ts
@@ -34,6 +34,10 @@ import {
normalizeWorldLabsKeyPoolConfig,
WORLDLABS_KEY_POOL_CONFIG_KEY,
} from "@/lib/worldlabs/key-pool-config-shared";
+import {
+ normalizeThreeDModelAliasConfig,
+ THREED_MODEL_ALIAS_CONFIG_SETTING_KEY,
+} from "@/lib/model3d/model-alias-config";
const settingSchema = z.object({
key: z.string(),
@@ -260,6 +264,10 @@ export async function POST(request: Request) {
nextValue = normalizeWorldLabsKeyPoolConfig(value);
}
+ if (key === THREED_MODEL_ALIAS_CONFIG_SETTING_KEY) {
+ nextValue = normalizeThreeDModelAliasConfig(value);
+ }
+
const setting = await upsertSystemSetting({
key,
value: nextValue,
diff --git a/app/api/canvas/3d/tasks/route.ts b/app/api/canvas/3d/tasks/route.ts
index c9565139a..ad4b02089 100644
--- a/app/api/canvas/3d/tasks/route.ts
+++ b/app/api/canvas/3d/tasks/route.ts
@@ -5,6 +5,8 @@ import {
getPointsSystemConfig,
getUserById,
} from "@/lib/db/queries";
+import { resolveThreeDModelRealId } from "@/lib/model3d/model-alias-config";
+import { getThreeDModelAliasConfigForRuntime } from "@/lib/model3d/model-alias-config-server";
import { getTripoProviderConfig } from "@/lib/tripo/server";
import { TripoApiError, tripoRequest } from "@/lib/tripo/client";
@@ -87,6 +89,11 @@ function hasOwn(data: Record
, key: string) {
return Object.prototype.hasOwnProperty.call(data, key);
}
+function readOptionalTrimmedText(input: unknown) {
+ const value = String(input || "").trim();
+ return value || null;
+}
+
function assignMapped(
payload: Record,
source: Record,
@@ -121,6 +128,7 @@ export async function POST(request: Request) {
if (!taskType) {
return NextResponse.json({ success: false, error: "缺少 taskType" }, { status: 400 });
}
+ const modelAliasConfig = await getThreeDModelAliasConfigForRuntime();
const payload: Record = {
type: taskType,
@@ -296,7 +304,6 @@ export async function POST(request: Request) {
}
assignMapped(payload, body as Record, {
- modelVersion: "model_version",
negativePrompt: "negative_prompt",
faceLimit: "face_limit",
texture: "texture",
@@ -335,6 +342,17 @@ export async function POST(request: Request) {
animateInPlace: "animate_in_place",
});
+ if (
+ taskType === "text_to_model" ||
+ taskType === "image_to_model" ||
+ taskType === "multiview_to_model"
+ ) {
+ payload.model_version = resolveThreeDModelRealId(
+ readOptionalTrimmedText(body.modelVersion),
+ modelAliasConfig.tripoModels,
+ );
+ }
+
if (body.compress === true) {
payload.compress = "geometry";
}
diff --git a/app/api/canvas/[id]/model3d-assets/route.ts b/app/api/canvas/[id]/model3d-assets/route.ts
index 9b0102a90..a476a37b5 100644
--- a/app/api/canvas/[id]/model3d-assets/route.ts
+++ b/app/api/canvas/[id]/model3d-assets/route.ts
@@ -3,6 +3,8 @@ import { randomUUID } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
import { db } from "@/lib/db";
+import { resolveThreeDModelAliasId } from "@/lib/model3d/model-alias-config";
+import { getThreeDModelAliasConfigForRuntime } from "@/lib/model3d/model-alias-config-server";
import { canvasProject, model3dAsset } from "@/lib/db/schema";
type Model3DTaskStatus =
@@ -94,6 +96,22 @@ function readJsonObject(input: unknown): Record | null {
: null;
}
+function normalizeAssetPayloadModelAlias(
+ payload: unknown,
+ entries: Awaited<
+ ReturnType
+ >["worldModels"],
+) {
+ const objectValue = readJsonObject(payload);
+ if (!objectValue) return objectValue;
+
+ const nextModel = resolveThreeDModelAliasId(objectValue.model, entries);
+ return {
+ ...objectValue,
+ ...(nextModel ? { model: nextModel } : {}),
+ };
+}
+
function readProgress(input: unknown): number | null {
if (input === undefined || input === null || input === "") return null;
const value = Number(input);
@@ -155,8 +173,27 @@ export async function GET(
)
.orderBy(desc(model3dAsset.updatedAt))
.limit(TASK_LIST_LIMIT);
+ const modelAliasConfig = await getThreeDModelAliasConfigForRuntime();
+ const normalizedAssets = assets.map((asset) => {
+ const entries =
+ asset.provider === "worldlabs-3d"
+ ? modelAliasConfig.worldModels
+ : modelAliasConfig.tripoModels;
- return NextResponse.json({ success: true, data: assets });
+ return {
+ ...asset,
+ modelVersion: resolveThreeDModelAliasId(asset.modelVersion, entries),
+ assetPayload:
+ asset.provider === "worldlabs-3d"
+ ? normalizeAssetPayloadModelAlias(
+ asset.assetPayload,
+ modelAliasConfig.worldModels,
+ )
+ : asset.assetPayload,
+ };
+ });
+
+ return NextResponse.json({ success: true, data: normalizedAssets });
} catch (error) {
console.error("[Model3DAssets] GET failed:", error);
return NextResponse.json({ success: false, error: "获取 3D 资产失败" }, { status: 500 });
@@ -186,18 +223,30 @@ export async function POST(
return NextResponse.json({ success: false, error: "缺少 taskId" }, { status: 400 });
}
+ const modelAliasConfig = await getThreeDModelAliasConfigForRuntime();
const status = normalizeTaskStatus(rawAsset.status);
const mode = normalizeTaskMode(rawAsset.mode);
const provider = readProvider(rawAsset.provider);
const prompt = String(rawAsset.prompt || "").trim();
- const modelVersion = String(rawAsset.modelVersion || "").trim();
+ const modelVersion = resolveThreeDModelAliasId(
+ rawAsset.modelVersion,
+ provider === "worldlabs-3d"
+ ? modelAliasConfig.worldModels
+ : modelAliasConfig.tripoModels,
+ );
const progress = readProgress(rawAsset.progress);
const modelUrl = readOptionalText(rawAsset.modelUrl);
const previewImageUrl = readOptionalText(rawAsset.previewImageUrl);
const error = readOptionalText(rawAsset.error);
const sourceTaskId = readOptionalText(rawAsset.sourceTaskId);
const exportFormat = readOptionalText(rawAsset.exportFormat);
- const assetPayload = readJsonObject(rawAsset.assetPayload);
+ const assetPayload =
+ provider === "worldlabs-3d"
+ ? normalizeAssetPayloadModelAlias(
+ rawAsset.assetPayload,
+ modelAliasConfig.worldModels,
+ )
+ : readJsonObject(rawAsset.assetPayload);
const createdAt = readDate(rawAsset.createdAt);
const now = new Date();
diff --git a/app/api/canvas/model-aliases/route.ts b/app/api/canvas/model-aliases/route.ts
new file mode 100644
index 000000000..ab6b19c5d
--- /dev/null
+++ b/app/api/canvas/model-aliases/route.ts
@@ -0,0 +1,23 @@
+import { NextResponse } from "next/server";
+import { getThreeDModelAliasConfigForRuntime } from "@/lib/model3d/model-alias-config-server";
+import { toPublicThreeDModelAliasConfig } from "@/lib/model3d/model-alias-config";
+
+export async function GET() {
+ try {
+ const config = await getThreeDModelAliasConfigForRuntime();
+
+ return NextResponse.json({
+ success: true,
+ data: toPublicThreeDModelAliasConfig(config),
+ });
+ } catch (error) {
+ console.error("[ThreeDModelAliases] GET failed:", error);
+ return NextResponse.json(
+ {
+ success: false,
+ error: "获取 3D 模型显示映射失败",
+ },
+ { status: 500 },
+ );
+ }
+}
diff --git a/app/api/canvas/world/tasks/[taskId]/route.ts b/app/api/canvas/world/tasks/[taskId]/route.ts
index a1a2906c5..818fe50f0 100644
--- a/app/api/canvas/world/tasks/[taskId]/route.ts
+++ b/app/api/canvas/world/tasks/[taskId]/route.ts
@@ -1,5 +1,7 @@
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";
@@ -169,12 +171,18 @@ function extractWorldFromOperation(operation: WorldLabsOperation) {
return null;
}
-function buildAssetPayload(world: WorldLabsWorld, assets?: WorldLabsWorldAssets | 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: getStringValue(world.model),
+ model: resolveThreeDModelAliasId(getStringValue(world.model), worldModelAliases),
caption: getStringValue(assets?.caption),
thumbnailUrl: getStringValue(assets?.thumbnail_url),
panoUrl: getStringValue(assets?.imagery?.pano_url),
@@ -210,6 +218,7 @@ export async function GET(request: Request, { params }: RouteParams) {
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)}`,
@@ -297,7 +306,7 @@ export async function GET(request: Request, { params }: RouteParams) {
const assetPayload = {
providerId: config.providerId || null,
providerName: config.providerName || null,
- ...buildAssetPayload(world, assets),
+ ...buildAssetPayload(world, modelAliasConfig.worldModels, assets),
};
return NextResponse.json({
diff --git a/app/api/canvas/world/tasks/route.ts b/app/api/canvas/world/tasks/route.ts
index a25aea46d..9d196e95c 100644
--- a/app/api/canvas/world/tasks/route.ts
+++ b/app/api/canvas/world/tasks/route.ts
@@ -5,6 +5,8 @@ import {
getPointsSystemConfig,
getUserById,
} from "@/lib/db/queries";
+import { resolveThreeDModelRealId } from "@/lib/model3d/model-alias-config";
+import { getThreeDModelAliasConfigForRuntime } from "@/lib/model3d/model-alias-config-server";
import { WorldLabsApiError, worldLabsRequest } from "@/lib/worldlabs/client";
import {
disableWorldLabsProvider,
@@ -152,6 +154,7 @@ export async function POST(request: Request) {
if (!["text", "image", "multi-image", "video"].includes(mode)) {
return NextResponse.json({ success: false, error: "缺少有效的 mode" }, { status: 400 });
}
+ const modelAliasConfig = await getThreeDModelAliasConfigForRuntime();
const pointsConfig = await getPointsSystemConfig();
const shouldChargePoints = shouldCharge3DPoints(mode);
@@ -190,6 +193,10 @@ export async function POST(request: Request) {
readOptionalTrimmedText(body.displayName) ||
readOptionalTrimmedText(body.prompt) ||
`World ${new Date().toLocaleString("zh-CN")}`;
+ const resolvedWorldModel = resolveThreeDModelRealId(
+ readOptionalTrimmedText(body.model),
+ modelAliasConfig.worldModels,
+ );
const worldPrompt = buildWorldPrompt(body, mode);
const candidates = preferredProviderId
@@ -212,7 +219,7 @@ export async function POST(request: Request) {
path: "/marble/v1/worlds:generate",
json: {
display_name: displayName,
- model: readOptionalTrimmedText(body.model) || "marble-1.1",
+ model: resolvedWorldModel || "marble-1.1",
permission: {
public: body.makePublic === true,
},
diff --git a/app/canvas/[id]/model3d/page.tsx b/app/canvas/[id]/model3d/page.tsx
index 21e3e256a..761aa2bd6 100644
--- a/app/canvas/[id]/model3d/page.tsx
+++ b/app/canvas/[id]/model3d/page.tsx
@@ -91,6 +91,12 @@ import {
getSortedWorldLabsSplatEntries,
pickBestWorldLabsSplatUrl,
} from "@/lib/worldlabs/splat-utils";
+import {
+ DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG,
+ getDefaultThreeDModelAliasId,
+ resolveThreeDModelDisplayName,
+ type PublicThreeDModelAliasConfig,
+} from "@/lib/model3d/model-alias-config";
import {
canvasService,
type Model3DTaskStatus,
@@ -175,31 +181,6 @@ type WorldImageSlot = {
azimuth: number;
};
-const MODEL_VERSION_OPTIONS = [
- "Turbo-v1.0-20250506",
- "v3.1-20260211",
- "v3.0-20250812",
- "v2.5-20250123",
- "v2.0-20240919",
- "v1.4-20240625",
-] as const;
-
-const WORLDLABS_MODEL_OPTIONS = [
- "marble-1.1",
- "marble-1.1-plus",
- "marble-1.0",
- "marble-1.0-draft",
-] as const;
-const WORLD_MODEL_DISPLAY_LABELS: Record<
- (typeof WORLDLABS_MODEL_OPTIONS)[number],
- string
-> = {
- "marble-1.1": "标准质量",
- "marble-1.1-plus": "高保真",
- "marble-1.0": "兼容质量",
- "marble-1.0-draft": "快速草稿",
-};
-
const ENV_PRESET_OPTIONS: Model3DViewerSettings["envPreset"][] = [
"city",
"sunset",
@@ -549,11 +530,6 @@ function worldModeToTaskMode(mode: WorldGenerationMode): TaskMode {
}
}
-function getWorldModelDisplayLabel(value?: string | null) {
- const normalized = String(value || "").trim() as (typeof WORLDLABS_MODEL_OPTIONS)[number];
- return WORLD_MODEL_DISPLAY_LABELS[normalized] || "标准质量";
-}
-
export default function CanvasModel3DPage() {
const router = useRouter();
const routeParams = useParams();
@@ -570,9 +546,11 @@ export default function CanvasModel3DPage() {
const [mode, setMode] = useState("text");
const [prompt, setPrompt] = useState("");
const [negativePrompt, setNegativePrompt] = useState("");
- const [modelVersion, setModelVersion] = useState<
- (typeof MODEL_VERSION_OPTIONS)[number]
- >("Turbo-v1.0-20250506");
+ const [modelVersion, setModelVersion] = useState(
+ getDefaultThreeDModelAliasId(
+ DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG.tripoModels,
+ ),
+ );
const [faceLimit, setFaceLimit] = useState(120_000);
const [texture, setTexture] = useState(true);
const [pbr, setPbr] = useState(true);
@@ -610,9 +588,11 @@ export default function CanvasModel3DPage() {
const [worldMode, setWorldMode] = useState("text");
const [worldPrompt, setWorldPrompt] = useState("");
const [worldDisplayName, setWorldDisplayName] = useState("");
- const [worldModel, setWorldModel] = useState<
- (typeof WORLDLABS_MODEL_OPTIONS)[number]
- >("marble-1.1");
+ const [worldModel, setWorldModel] = useState(
+ getDefaultThreeDModelAliasId(
+ DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG.worldModels,
+ ),
+ );
const [worldDisableRecaption, setWorldDisableRecaption] = useState(false);
const [worldIsPano, setWorldIsPano] = useState(false);
const [worldMakePublic, setWorldMakePublic] = useState(false);
@@ -695,6 +675,10 @@ export default function CanvasModel3DPage() {
// Fetch site settings for Logo
const { data: siteSettingsData } = useSWR("/api/site-settings", fetcher);
+ const { data: modelAliasConfigResponse } = useSWR<{
+ success?: boolean;
+ data?: PublicThreeDModelAliasConfig;
+ }>("/api/canvas/model-aliases", fetcher);
const { data: membershipSummary } = useSWR<{
summary?: {
currentCredits?: number;
@@ -724,12 +708,65 @@ export default function CanvasModel3DPage() {
// Display logo logic (prefer dark mode logo since we are in dark theme)
const displayLogo =
siteLogoDark && siteLogoDark.trim() !== "" ? siteLogoDark : siteLogo;
+ const publicModelAliasConfig =
+ modelAliasConfigResponse?.data || DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG;
+ const tripoModelOptions = useMemo(
+ () => {
+ const enabledEntries = publicModelAliasConfig.tripoModels.filter(
+ (item) => item.enabled !== false,
+ );
+ return enabledEntries.length > 0
+ ? enabledEntries
+ : publicModelAliasConfig.tripoModels;
+ },
+ [publicModelAliasConfig.tripoModels],
+ );
+ const worldModelOptions = useMemo(
+ () => {
+ const enabledEntries = publicModelAliasConfig.worldModels.filter(
+ (item) => item.enabled !== false,
+ );
+ return enabledEntries.length > 0
+ ? enabledEntries
+ : publicModelAliasConfig.worldModels;
+ },
+ [publicModelAliasConfig.worldModels],
+ );
+ const getWorldModelDisplayLabel = useCallback(
+ (value?: string | null) =>
+ resolveThreeDModelDisplayName(
+ value,
+ publicModelAliasConfig.worldModels,
+ "标准质量",
+ ),
+ [publicModelAliasConfig.worldModels],
+ );
// Effect hooks for data loading and cleanup
useEffect(() => {
console.log("CanvasModel3DPage mounted - v3.0 Dark Mode");
}, []);
+ useEffect(() => {
+ const nextDefaultTripoModelId = getDefaultThreeDModelAliasId(
+ publicModelAliasConfig.tripoModels,
+ );
+ if (!nextDefaultTripoModelId) return;
+ if (!tripoModelOptions.some((item) => item.id === modelVersion)) {
+ setModelVersion(nextDefaultTripoModelId);
+ }
+ }, [modelVersion, publicModelAliasConfig.tripoModels, tripoModelOptions]);
+
+ useEffect(() => {
+ const nextDefaultWorldModelId = getDefaultThreeDModelAliasId(
+ publicModelAliasConfig.worldModels,
+ );
+ if (!nextDefaultWorldModelId) return;
+ if (!worldModelOptions.some((item) => item.id === worldModel)) {
+ setWorldModel(nextDefaultWorldModelId);
+ }
+ }, [publicModelAliasConfig.worldModels, worldModel, worldModelOptions]);
+
useEffect(() => {
const html = document.documentElement;
const body = document.body;
@@ -2550,13 +2587,13 @@ export default function CanvasModel3DPage() {
- {MODEL_VERSION_OPTIONS.map((v) => (
+ {tripoModelOptions.map((option) => (
- {v}
+ {option.displayName}
))}
@@ -2826,13 +2863,13 @@ export default function CanvasModel3DPage() {
- {WORLDLABS_MODEL_OPTIONS.map((item) => (
+ {worldModelOptions.map((item) => (
- {getWorldModelDisplayLabel(item)}
+ {item.displayName}
))}
diff --git a/components/canvas/nodes/Model3DNode.tsx b/components/canvas/nodes/Model3DNode.tsx
index bf78ca67b..01fae7fa6 100644
--- a/components/canvas/nodes/Model3DNode.tsx
+++ b/components/canvas/nodes/Model3DNode.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useState } from "react";
+import React, { useMemo, useState } from "react";
import {
AlertCircle,
Box,
@@ -21,10 +21,16 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
+import useSWR from "swr";
import { toast } from "sonner";
+import {
+ DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG,
+ resolveThreeDModelDisplayName,
+ type PublicThreeDModelAliasConfig,
+} from "@/lib/model3d/model-alias-config";
import { useCanvasStore } from "@/store/canvas";
import type { CanvasNode, Model3DNodeData } from "@/types/canvas";
-import { cn } from "@/lib/utils";
+import { cn, fetcher } from "@/lib/utils";
interface Model3DNodeProps {
node: CanvasNode;
@@ -68,8 +74,23 @@ export function Model3DNode({ node, isSelected }: Model3DNodeProps) {
const data = node.data as Model3DNodeData;
const [isHovered, setIsHovered] = useState(false);
const deleteNode = useCanvasStore((state) => state.deleteNode);
+ const { data: modelAliasConfigResponse } = useSWR<{
+ success?: boolean;
+ data?: PublicThreeDModelAliasConfig;
+ }>("/api/canvas/model-aliases", fetcher);
const providerLabel =
data.metadata?.provider === "worldlabs-3d" ? "World Labs 结果" : "Tripo3D 结果";
+ const publicModelAliasConfig =
+ modelAliasConfigResponse?.data || DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG;
+ const modelVersionLabel = useMemo(
+ () =>
+ resolveThreeDModelDisplayName(
+ data.modelVersion,
+ publicModelAliasConfig.tripoModels,
+ providerLabel,
+ ),
+ [data.modelVersion, providerLabel, publicModelAliasConfig.tripoModels],
+ );
const worldAssetPayload = readWorldAssetPayload(data);
const worldUrl = readOptionalUrl(worldAssetPayload?.worldUrl);
const worldImageUrl =
@@ -178,7 +199,7 @@ export function Model3DNode({ node, isSelected }: Model3DNodeProps) {
{data.prompt || inferModelFileName(data, node.id)}
- {data.modelVersion ? `模型: ${data.modelVersion}` : providerLabel}
+ {data.modelVersion ? `模型: ${modelVersionLabel}` : providerLabel}
diff --git a/components/canvas/nodes/World3DNode.tsx b/components/canvas/nodes/World3DNode.tsx
index 30d969e1d..d5728fec8 100644
--- a/components/canvas/nodes/World3DNode.tsx
+++ b/components/canvas/nodes/World3DNode.tsx
@@ -24,10 +24,16 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
+import useSWR from "swr";
+import {
+ DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG,
+ resolveThreeDModelDisplayName,
+ type PublicThreeDModelAliasConfig,
+} from "@/lib/model3d/model-alias-config";
import { toast } from "sonner";
import { useCanvasStore } from "@/store/canvas";
import type { CanvasNode, World3DHotspot, World3DNodeData, World3DViewMode } from "@/types/canvas";
-import { cn } from "@/lib/utils";
+import { cn, fetcher } from "@/lib/utils";
interface World3DNodeProps {
node: CanvasNode;
@@ -44,24 +50,6 @@ const VIEW_META: Array<{
{ id: "mesh", label: "M", icon: Box },
];
-function getWorldNodeModelLabel(value?: string | null) {
- const normalized = String(value || "").trim();
- switch (normalized) {
- case "marble-1.1":
- return "标准质量";
- case "marble-1.1-plus":
- return "高保真";
- case "marble-1.0":
- return "兼容质量";
- case "marble-1.0-draft":
- return "快速草稿";
- case "":
- return "原生世界节点";
- default:
- return "自定义质量";
- }
-}
-
function getWorldNodeViewLabel(view: World3DViewMode) {
switch (view) {
case "splat":
@@ -134,9 +122,24 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
const [interactionMode, setInteractionMode] = useState<"navigate" | "hotspot">("navigate");
const deleteNode = useCanvasStore((state) => state.deleteNode);
const updateNode = useCanvasStore((state) => state.updateNode);
+ const { data: modelAliasConfigResponse } = useSWR<{
+ success?: boolean;
+ data?: PublicThreeDModelAliasConfig;
+ }>("/api/canvas/model-aliases", fetcher);
const activeView = useMemo(() => resolveActiveView(data), [data]);
const availableViews = useMemo(() => resolveAvailableViews(data), [data]);
+ const publicModelAliasConfig =
+ modelAliasConfigResponse?.data || DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG;
+ const worldModelLabel = useMemo(
+ () =>
+ resolveThreeDModelDisplayName(
+ data.modelVersion,
+ publicModelAliasConfig.worldModels,
+ "原生世界节点",
+ ),
+ [data.modelVersion, publicModelAliasConfig.worldModels],
+ );
const primaryUrl = resolvePrimaryUrl(data, activeView);
const downloadTarget = inferDownloadTarget(data, activeView);
const hotspotCount = data.hotspots?.length || 0;
@@ -281,7 +284,7 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
- {getWorldNodeModelLabel(data.modelVersion)}
+ {worldModelLabel}
diff --git a/lib/model3d/model-alias-config-server.ts b/lib/model3d/model-alias-config-server.ts
new file mode 100644
index 000000000..753ab369e
--- /dev/null
+++ b/lib/model3d/model-alias-config-server.ts
@@ -0,0 +1,13 @@
+import { getSystemSetting } from "@/lib/db/queries";
+import {
+ normalizeThreeDModelAliasConfig,
+ THREED_MODEL_ALIAS_CONFIG_SETTING_KEY,
+} from "@/lib/model3d/model-alias-config";
+
+export async function getThreeDModelAliasConfigForRuntime() {
+ const setting = await getSystemSetting({
+ key: THREED_MODEL_ALIAS_CONFIG_SETTING_KEY,
+ }).catch(() => null);
+
+ return normalizeThreeDModelAliasConfig(setting?.value);
+}
diff --git a/lib/model3d/model-alias-config.ts b/lib/model3d/model-alias-config.ts
new file mode 100644
index 000000000..d50486e26
--- /dev/null
+++ b/lib/model3d/model-alias-config.ts
@@ -0,0 +1,315 @@
+export const THREED_MODEL_ALIAS_CONFIG_SETTING_KEY = "threed_model_alias_config";
+
+export type ThreeDModelAliasEntry = {
+ id: string;
+ displayName: string;
+ realModelId: string;
+ description: string;
+ enabled: boolean;
+ isDefault: boolean;
+};
+
+export type ThreeDModelAliasConfig = {
+ tripoModels: ThreeDModelAliasEntry[];
+ worldModels: ThreeDModelAliasEntry[];
+};
+
+export type PublicThreeDModelAliasEntry = Omit<
+ ThreeDModelAliasEntry,
+ "realModelId"
+>;
+
+export type PublicThreeDModelAliasConfig = {
+ tripoModels: PublicThreeDModelAliasEntry[];
+ worldModels: PublicThreeDModelAliasEntry[];
+};
+
+const DEFAULT_TRIPO_MODEL_ALIASES: ThreeDModelAliasEntry[] = [
+ {
+ id: "tripo-standard",
+ displayName: "标准模型",
+ realModelId: "Turbo-v1.0-20250506",
+ description: "默认高速高质量模型,适合大多数 3D 生成场景。",
+ enabled: true,
+ isDefault: true,
+ },
+ {
+ id: "tripo-detail",
+ displayName: "高精模型",
+ realModelId: "v3.1-20260211",
+ description: "偏向细节质量和结构还原,适合高要求模型生成。",
+ enabled: true,
+ isDefault: false,
+ },
+ {
+ id: "tripo-balance",
+ displayName: "均衡模型",
+ realModelId: "v3.0-20250812",
+ description: "速度与质量更均衡,适合日常生产。",
+ enabled: true,
+ isDefault: false,
+ },
+ {
+ id: "tripo-classic",
+ displayName: "经典模型",
+ realModelId: "v2.5-20250123",
+ description: "兼顾稳定性与兼容性,适合保守方案。",
+ enabled: true,
+ isDefault: false,
+ },
+ {
+ id: "tripo-stable",
+ displayName: "稳定模型",
+ realModelId: "v2.0-20240919",
+ description: "老版本稳定模型,适合兼容历史流程。",
+ enabled: true,
+ isDefault: false,
+ },
+ {
+ id: "tripo-legacy",
+ displayName: "兼容模型",
+ realModelId: "v1.4-20240625",
+ description: "更旧的兼容模型,适合特定老任务回放。",
+ enabled: true,
+ isDefault: false,
+ },
+];
+
+const DEFAULT_WORLD_MODEL_ALIASES: ThreeDModelAliasEntry[] = [
+ {
+ id: "world-standard",
+ displayName: "标准质量",
+ realModelId: "marble-1.1",
+ description: "默认世界生成质量,适合大多数空间创建。",
+ enabled: true,
+ isDefault: true,
+ },
+ {
+ id: "world-fidelity",
+ displayName: "高保真",
+ realModelId: "marble-1.1-plus",
+ description: "更高保真与细节表现,适合高质量空间生成。",
+ enabled: true,
+ isDefault: false,
+ },
+ {
+ id: "world-compatible",
+ displayName: "兼容质量",
+ realModelId: "marble-1.0",
+ description: "兼容旧版世界任务和历史工作流。",
+ enabled: true,
+ isDefault: false,
+ },
+ {
+ id: "world-draft",
+ displayName: "快速草稿",
+ realModelId: "marble-1.0-draft",
+ description: "更适合快速预览与草稿生成。",
+ enabled: true,
+ isDefault: false,
+ },
+];
+
+function sanitizeText(value: unknown) {
+ return String(value || "").trim();
+}
+
+function cloneEntries(entries: ThreeDModelAliasEntry[]) {
+ return entries.map((entry) => ({ ...entry }));
+}
+
+function normalizeEntry(
+ value: unknown,
+ index: number,
+ defaults: ThreeDModelAliasEntry[],
+ prefix: string,
+): ThreeDModelAliasEntry {
+ const fallback = defaults[index] || defaults[0];
+ const hasRawObject =
+ value && typeof value === "object" && !Array.isArray(value);
+ const raw = hasRawObject ? (value as Record