diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx
index fe9f484f3..a54f61a3f 100644
--- a/app/(admin)/settings/admin-settings-client.tsx
+++ b/app/(admin)/settings/admin-settings-client.tsx
@@ -63,6 +63,11 @@ import {
PRODUCT_PLANNING_AI_MODEL_DEFAULT_MODEL_ID,
type ProductPlanningAiModelConfig,
} from "@/lib/product-planning/model-config";
+import {
+ defaultWorldLabsKeyPoolConfig,
+ normalizeWorldLabsKeyPoolConfig,
+ WORLDLABS_KEY_POOL_CONFIG_KEY,
+} from "@/lib/worldlabs/key-pool-config-shared";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator";
import { HomeAnimationEmbed } from "@/components/home/home-animation-embed";
@@ -828,6 +833,13 @@ export function AdminSettingsClient() {
`/api/admin/settings?key=${PRODUCT_PLANNING_AI_MODEL_CONFIG_KEY}`,
fetcher
);
+ const {
+ data: worldlabsKeyPoolConfigData,
+ mutate: mutateWorldlabsKeyPoolConfig,
+ } = useSWR<{ setting?: { value?: unknown } }>(
+ `/api/admin/settings?key=${WORLDLABS_KEY_POOL_CONFIG_KEY}`,
+ fetcher
+ );
const { data: enhanceProConfigData, mutate: mutateEnhanceProConfig } =
useSWR<{ setting?: { value?: unknown } }>(
`/api/admin/settings?key=${ENHANCE_PRO_KREA_SETTING_KEY}`,
@@ -1352,6 +1364,13 @@ export function AdminSettingsClient() {
apiKey: "",
isEnabled: true,
});
+ const [worldlabsKeyPoolConfig, setWorldlabsKeyPoolConfig] = useState(
+ defaultWorldLabsKeyPoolConfig
+ );
+ const [worldlabsProviderSearch, setWorldlabsProviderSearch] = useState("");
+ const [worldlabsProviderBusyId, setWorldlabsProviderBusyId] = useState<
+ string | null
+ >(null);
const [moonshotDetailAgentConfig, setMoonshotDetailAgentConfig] = useState({
providerId: "",
modelEntryId: "",
@@ -1855,6 +1874,34 @@ export function AdminSettingsClient() {
});
}, [providersData?.providers]);
+ useEffect(() => {
+ setWorldlabsKeyPoolConfig(
+ normalizeWorldLabsKeyPoolConfig(worldlabsKeyPoolConfigData?.setting?.value)
+ );
+ }, [worldlabsKeyPoolConfigData]);
+
+ const worldlabsProviders = useMemo(
+ () =>
+ (providersData?.providers || []).filter(
+ (provider) => provider.type === "worldlabs-3d"
+ ),
+ [providersData?.providers]
+ );
+ const filteredWorldlabsProviders = useMemo(() => {
+ const keyword = worldlabsProviderSearch.trim().toLowerCase();
+ if (!keyword) return worldlabsProviders;
+
+ return worldlabsProviders.filter((provider) =>
+ [provider.name, provider.apiKey, provider.baseUrl]
+ .filter(Boolean)
+ .some((value) => value.toLowerCase().includes(keyword))
+ );
+ }, [worldlabsProviderSearch, worldlabsProviders]);
+ const enabledWorldlabsProviderCount = useMemo(
+ () => worldlabsProviders.filter((provider) => provider.isEnabled).length,
+ [worldlabsProviders]
+ );
+
// 导航链接状态
const [navLinkForm, setNavLinkForm] = useState({
id: "",
@@ -4130,12 +4177,74 @@ export function AdminSettingsClient() {
throw new Error("保存失败");
}
+ const keyPoolResponse = await fetch("/api/admin/settings", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ key: WORLDLABS_KEY_POOL_CONFIG_KEY,
+ value: normalizeWorldLabsKeyPoolConfig(worldlabsKeyPoolConfig),
+ }),
+ });
+
+ if (!keyPoolResponse.ok) {
+ const payload = await keyPoolResponse.json().catch(() => ({}));
+ throw new Error(
+ String(payload?.message || payload?.error || "保存 World Labs key 池导入令牌失败")
+ );
+ }
+
toast.success("World Labs 配置保存成功");
setWorldlabs3dConfig((prev) => ({ ...prev, apiKey: "" }));
mutateProviders();
+ mutateWorldlabsKeyPoolConfig();
} catch (error) {
console.error("保存 World Labs 配置失败:", error);
- toast.error("保存 World Labs 配置失败");
+ toast.error(
+ error instanceof Error ? error.message : "保存 World Labs 配置失败"
+ );
+ }
+ };
+
+ const handleEditWorldlabsPoolProvider = (provider: AiProvider) => {
+ setProviderForm({
+ id: provider.id,
+ name: provider.name,
+ type: provider.type,
+ baseUrl: provider.baseUrl,
+ apiKey: "",
+ isEnabled: provider.isEnabled,
+ });
+ setEditingProvider(provider.id);
+ setShowProviderDialog(true);
+ };
+
+ const handleToggleWorldlabsPoolProvider = async (
+ provider: AiProvider,
+ isEnabled: boolean
+ ) => {
+ setWorldlabsProviderBusyId(provider.id);
+ try {
+ const response = await fetch("/api/admin/ai-providers", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ id: provider.id,
+ isEnabled,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error("更新状态失败");
+ }
+
+ toast.success(
+ `已${isEnabled ? "启用" : "停用"} ${provider.name || "World Labs Key"}`
+ );
+ mutateProviders();
+ } catch (error) {
+ toast.error("更新 World Labs Key 状态失败");
+ } finally {
+ setWorldlabsProviderBusyId(null);
}
};
@@ -12461,6 +12570,124 @@ export function AdminSettingsClient() {
}
/>
+
+
+
+ setWorldlabsKeyPoolConfig({
+ importToken: e.target.value,
+ })
+ }
+ placeholder="供 /api/worldlabs/key-pool/import 调用的 Bearer Token"
+ />
+
+ 用于 Python 脚本通过
+
+ /api/worldlabs/key-pool/import
+
+ 批量导入新 Key,保存到数据库系统设置。
+
+
+
+
+
+
Key 池明细
+
+ 当前共 {worldlabsProviders.length} 个 Key,已启用{" "}
+ {enabledWorldlabsProviderCount} 个。
+
+
+
+
+ setWorldlabsProviderSearch(e.target.value)}
+ />
+
+
+
+
+ {filteredWorldlabsProviders.length ? (
+ filteredWorldlabsProviders.map((provider) => (
+
+
+
+
{provider.name}
+
+ {provider.isEnabled ? "已启用" : "已禁用"}
+
+
+
+ Key: {provider.apiKey}
+
+
+ API: {provider.baseUrl}
+
+
+ 创建时间: {formatBeijingTime(provider.createdAt)}
+
+
+
+
+
+
+ void handleToggleWorldlabsPoolProvider(
+ provider,
+ checked
+ )
+ }
+ />
+
+ {provider.isEnabled ? "启用中" : "已停用"}
+
+
+
+
+
+ ))
+ ) : (
+
+ {worldlabsProviders.length
+ ? "没有匹配到符合筛选条件的 World Labs Key。"
+ : "当前还没有导入任何 World Labs Key。"}
+
+ )}
+
+
diff --git a/app/api/admin/settings/route.ts b/app/api/admin/settings/route.ts
index f7c508fb6..9dcfe1bea 100644
--- a/app/api/admin/settings/route.ts
+++ b/app/api/admin/settings/route.ts
@@ -30,6 +30,10 @@ import {
ENHANCE_PRO_KREA_SETTING_KEY,
normalizeEnhanceProKreaConfig,
} from "@/lib/enhance-pro/config";
+import {
+ normalizeWorldLabsKeyPoolConfig,
+ WORLDLABS_KEY_POOL_CONFIG_KEY,
+} from "@/lib/worldlabs/key-pool-config-shared";
const settingSchema = z.object({
key: z.string(),
@@ -252,6 +256,10 @@ export async function POST(request: Request) {
nextValue = normalizedValue;
}
+ if (key === WORLDLABS_KEY_POOL_CONFIG_KEY) {
+ nextValue = normalizeWorldLabsKeyPoolConfig(value);
+ }
+
const setting = await upsertSystemSetting({
key,
value: nextValue,
diff --git a/app/canvas/[id]/model3d/page.tsx b/app/canvas/[id]/model3d/page.tsx
index 35859b39f..1a68259b5 100644
--- a/app/canvas/[id]/model3d/page.tsx
+++ b/app/canvas/[id]/model3d/page.tsx
@@ -13,7 +13,6 @@ import {
Eye,
EyeOff,
Grid3X3,
- History,
Home,
ImagePlus,
Images,
@@ -88,6 +87,10 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn, fetcher } from "@/lib/utils";
+import {
+ getSortedWorldLabsSplatEntries,
+ pickBestWorldLabsSplatUrl,
+} from "@/lib/worldlabs/splat-utils";
import {
canvasService,
type Model3DTaskStatus,
@@ -1611,9 +1614,7 @@ export default function CanvasModel3DPage() {
const isWorldTask = currentTask.provider === "worldlabs-3d";
const worldAssetPayload = currentTask.assetPayload ?? null;
const primarySplatUrl = isWorldTask
- ? Object.values(worldAssetPayload?.spzUrls || {}).find(
- (value): value is string => typeof value === "string" && value.trim().length > 0
- ) || null
+ ? pickBestWorldLabsSplatUrl(worldAssetPayload?.spzUrls)
: null;
const worldImageUrl =
worldAssetPayload?.panoUrl ||
@@ -2144,9 +2145,8 @@ export default function CanvasModel3DPage() {
currentTask?.provider === "worldlabs-3d" && currentTask?.shareEnabled
? buildWorldShareUrl(currentTask.shareToken)
: null;
- const currentSpzEntries = Object.entries(currentWorldAsset?.spzUrls || {});
- const currentPrimarySpzUrl =
- currentSpzEntries.find(([, url]) => typeof url === "string" && url.trim())?.[1] || null;
+ const currentSpzEntries = getSortedWorldLabsSplatEntries(currentWorldAsset?.spzUrls);
+ const currentPrimarySpzUrl = currentSpzEntries[0]?.[1] || null;
const currentWorldImageUrl =
currentWorldAsset?.panoUrl ||
currentWorldAsset?.thumbnailUrl ||
@@ -2163,22 +2163,6 @@ export default function CanvasModel3DPage() {
currentPrimarySpzUrl
)
: !currentModelUrl);
- const activeStatusLabel = currentTask
- ? statusLabel(currentTask.status)
- : "未开始";
- const activePromptSummary =
- currentTask?.prompt?.trim() ||
- (workspaceMode === "world" ? "尚未创建世界任务" : "尚未创建模型任务");
- const activeProviderLabel =
- currentTask?.provider === "worldlabs-3d" || workspaceMode === "world"
- ? "World Labs"
- : "Tripo3D";
- const activeRelativeTime = currentTask
- ? formatDistanceToNow(new Date(currentTask.createdAt), {
- addSuffix: true,
- locale: zhCN,
- })
- : null;
const availableWorldViewportModes = useMemo(() => {
const modes: WorldViewportMode[] = [];
if (currentPrimarySpzUrl) modes.push("splat");
@@ -2201,8 +2185,6 @@ export default function CanvasModel3DPage() {
workspaceMode === "world" ? "3D 世界工作台" : "3D 工作台";
const viewerEmptyStateTitle =
workspaceMode === "world" ? "准备生成 3D 世界" : "准备生成 3D 资产";
- const waitingTaskLabel =
- workspaceMode === "world" ? "等待创建世界任务" : "等待创建模型任务";
useEffect(() => {
if (workspaceMode !== "world") return;
setWorldViewportMode(preferredWorldViewportMode);
@@ -3362,40 +3344,6 @@ export default function CanvasModel3DPage() {
{/* 3. Main Viewer Area */}
-
-
-
- {activeStatusLabel}
-
-
- {activeProviderLabel}
-
- {workspaceMode === "world" && (
-
- 3D 世界
-
- )}
-
-
- {activePromptSummary}
-
-
-
- {activeRelativeTime || waitingTaskLabel}
- {currentWorldId && (
- <>
- •
- World {currentWorldId}
- >
- )}
-
-
{workspaceMode === "world" && (
{[
diff --git a/app/worlds/share/[token]/page.tsx b/app/worlds/share/[token]/page.tsx
index 6de958529..9b8368274 100644
--- a/app/worlds/share/[token]/page.tsx
+++ b/app/worlds/share/[token]/page.tsx
@@ -3,6 +3,7 @@ import { notFound } from "next/navigation";
import { WorldSharePage } from "@/components/canvas/world3d/WorldSharePage";
import { db } from "@/lib/db";
import { model3dAsset } from "@/lib/db/schema";
+import { pickBestWorldLabsSplatUrl } from "@/lib/worldlabs/splat-utils";
export const dynamic = "force-dynamic";
@@ -34,16 +35,6 @@ function readAssetPayload(
: null;
}
-function pickPrimarySplatUrl(assetPayload: ReturnType) {
- if (!assetPayload?.spzUrls || typeof assetPayload.spzUrls !== "object") return null;
- for (const value of Object.values(assetPayload.spzUrls)) {
- if (typeof value === "string" && value.trim()) {
- return value.trim();
- }
- }
- return null;
-}
-
export default async function SharedWorldPage({
params,
}: {
@@ -92,7 +83,7 @@ export default async function SharedWorldPage({
prompt={asset.prompt || null}
semanticsMetadata={assetPayload?.semanticsMetadata || null}
shareUrl={sharePath}
- splatUrl={pickPrimarySplatUrl(assetPayload)}
+ splatUrl={pickBestWorldLabsSplatUrl(assetPayload?.spzUrls)}
title={assetPayload?.displayName || asset.prompt || `World ${asset.taskId.slice(0, 8)}`}
worldUrl={assetPayload?.worldUrl || null}
/>
diff --git a/components/canvas/world3d/WorldSharePage.tsx b/components/canvas/world3d/WorldSharePage.tsx
index 35a92b020..180bfa5de 100644
--- a/components/canvas/world3d/WorldSharePage.tsx
+++ b/components/canvas/world3d/WorldSharePage.tsx
@@ -3,7 +3,6 @@
import {
Box,
Download,
- ExternalLink,
Globe,
ImageIcon,
Link2,
@@ -147,16 +146,6 @@ export function WorldSharePage({
{copied ? "已复制链接" : "复制分享链接"}
- {worldUrl ? (
-
- ) : null}
{meshUrl ? (