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 ? (