feat: improve worldlabs admin controls and splat selection
This commit is contained in:
parent
c4aafe9413
commit
b3b49b4fe2
@ -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() {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="worldlabsKeyPoolImportToken">
|
||||
Key 池导入令牌
|
||||
</Label>
|
||||
<Input
|
||||
id="worldlabsKeyPoolImportToken"
|
||||
type="password"
|
||||
value={worldlabsKeyPoolConfig.importToken}
|
||||
onChange={(e) =>
|
||||
setWorldlabsKeyPoolConfig({
|
||||
importToken: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="供 /api/worldlabs/key-pool/import 调用的 Bearer Token"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
用于 Python 脚本通过
|
||||
<code className="mx-1 rounded bg-muted px-1 py-0.5 text-[11px]">
|
||||
/api/worldlabs/key-pool/import
|
||||
</code>
|
||||
批量导入新 Key,保存到数据库系统设置。
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4 rounded-xl border p-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium">Key 池明细</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
当前共 {worldlabsProviders.length} 个 Key,已启用{" "}
|
||||
{enabledWorldlabsProviderCount} 个。
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative w-full md:max-w-xs">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
className="pl-9"
|
||||
placeholder="搜索名称 / Key / API 地址"
|
||||
value={worldlabsProviderSearch}
|
||||
onChange={(e) => setWorldlabsProviderSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{filteredWorldlabsProviders.length ? (
|
||||
filteredWorldlabsProviders.map((provider) => (
|
||||
<div
|
||||
key={provider.id}
|
||||
className="flex flex-col gap-4 rounded-lg border p-4 lg:flex-row lg:items-center lg:justify-between"
|
||||
>
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="font-medium">{provider.name}</div>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-xs",
|
||||
provider.isEnabled
|
||||
? "bg-emerald-500/10 text-emerald-600"
|
||||
: "bg-zinc-200 text-zinc-600"
|
||||
)}
|
||||
>
|
||||
{provider.isEnabled ? "已启用" : "已禁用"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-mono text-xs text-muted-foreground">
|
||||
Key: {provider.apiKey}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
API: {provider.baseUrl}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
创建时间: {formatBeijingTime(provider.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditWorldlabsPoolProvider(provider)}
|
||||
>
|
||||
<Edit className="mr-1 h-4 w-4" />
|
||||
编辑
|
||||
</Button>
|
||||
<div className="flex items-center gap-2 rounded-md border px-3 py-2">
|
||||
<Switch
|
||||
checked={provider.isEnabled}
|
||||
disabled={worldlabsProviderBusyId === provider.id}
|
||||
onCheckedChange={(checked) =>
|
||||
void handleToggleWorldlabsPoolProvider(
|
||||
provider,
|
||||
checked
|
||||
)
|
||||
}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{provider.isEnabled ? "启用中" : "已停用"}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteProvider(provider.id)}
|
||||
>
|
||||
<Trash2 className="mr-1 h-4 w-4" />
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
|
||||
{worldlabsProviders.length
|
||||
? "没有匹配到符合筛选条件的 World Labs Key。"
|
||||
: "当前还没有导入任何 World Labs Key。"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-sm">启用 World Labs</Label>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 */}
|
||||
<div className="relative flex-1 bg-black">
|
||||
<div className="absolute left-6 top-6 z-10 max-w-md rounded-2xl border border-white/10 bg-zinc-900/75 p-4 shadow-2xl backdrop-blur-md">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant={currentTask?.status === "success" ? "default" : "secondary"}>
|
||||
{activeStatusLabel}
|
||||
</Badge>
|
||||
<Badge
|
||||
className="border-white/10 bg-white/5 text-zinc-300"
|
||||
variant="outline"
|
||||
>
|
||||
{activeProviderLabel}
|
||||
</Badge>
|
||||
{workspaceMode === "world" && (
|
||||
<Badge
|
||||
className="border-sky-500/30 bg-sky-500/10 text-sky-300"
|
||||
variant="outline"
|
||||
>
|
||||
3D 世界
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-3 line-clamp-2 text-sm text-white">
|
||||
{activePromptSummary}
|
||||
</p>
|
||||
<div className="mt-2 flex items-center gap-2 text-[11px] text-zinc-500">
|
||||
<History className="h-3.5 w-3.5" />
|
||||
<span>{activeRelativeTime || waitingTaskLabel}</span>
|
||||
{currentWorldId && (
|
||||
<>
|
||||
<span className="text-zinc-700">•</span>
|
||||
<span className="truncate">World {currentWorldId}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{workspaceMode === "world" && (
|
||||
<div className="absolute right-6 top-6 z-10 flex items-center gap-2 rounded-full border border-white/10 bg-zinc-900/75 p-2 shadow-2xl backdrop-blur-md">
|
||||
{[
|
||||
|
||||
@ -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<typeof readAssetPayload>) {
|
||||
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}
|
||||
/>
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
import {
|
||||
Box,
|
||||
Download,
|
||||
ExternalLink,
|
||||
Globe,
|
||||
ImageIcon,
|
||||
Link2,
|
||||
@ -147,16 +146,6 @@ export function WorldSharePage({
|
||||
<Link2 className="mr-2 h-4 w-4" />
|
||||
{copied ? "已复制链接" : "复制分享链接"}
|
||||
</Button>
|
||||
{worldUrl ? (
|
||||
<Button
|
||||
className="border-white/10 bg-white/5 text-zinc-100 hover:bg-white/10"
|
||||
onClick={() => window.open(worldUrl, "_blank", "noopener,noreferrer")}
|
||||
variant="outline"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
打开原始 World
|
||||
</Button>
|
||||
) : null}
|
||||
{meshUrl ? (
|
||||
<Button
|
||||
className="border-white/10 bg-white/5 text-zinc-100 hover:bg-white/10"
|
||||
|
||||
29
lib/worldlabs/key-pool-config-shared.ts
Normal file
29
lib/worldlabs/key-pool-config-shared.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export const WORLDLABS_KEY_POOL_CONFIG_KEY = "worldlabs_key_pool_config";
|
||||
|
||||
export type WorldLabsKeyPoolConfig = {
|
||||
importToken: string;
|
||||
};
|
||||
|
||||
export const defaultWorldLabsKeyPoolConfig: WorldLabsKeyPoolConfig = {
|
||||
importToken: "",
|
||||
};
|
||||
|
||||
function normalizeImportToken(value: unknown) {
|
||||
return String(value || "")
|
||||
.replace(/[\u2028\u2029]/g, "")
|
||||
.replace(/[\u0000-\u001F\u007F]/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function normalizeWorldLabsKeyPoolConfig(
|
||||
value: unknown
|
||||
): WorldLabsKeyPoolConfig {
|
||||
if (!value || typeof value !== "object") {
|
||||
return defaultWorldLabsKeyPoolConfig;
|
||||
}
|
||||
|
||||
const raw = value as Partial<WorldLabsKeyPoolConfig>;
|
||||
return {
|
||||
importToken: normalizeImportToken(raw.importToken),
|
||||
};
|
||||
}
|
||||
@ -1,37 +1,20 @@
|
||||
import "server-only";
|
||||
|
||||
import { getSystemSetting } from "@/lib/db/queries";
|
||||
import {
|
||||
defaultWorldLabsKeyPoolConfig,
|
||||
normalizeWorldLabsKeyPoolConfig,
|
||||
WORLDLABS_KEY_POOL_CONFIG_KEY,
|
||||
type WorldLabsKeyPoolConfig,
|
||||
} from "@/lib/worldlabs/key-pool-config-shared";
|
||||
|
||||
export const WORLDLABS_KEY_POOL_CONFIG_KEY = "worldlabs_key_pool_config";
|
||||
|
||||
export type WorldLabsKeyPoolConfig = {
|
||||
importToken: string;
|
||||
export {
|
||||
defaultWorldLabsKeyPoolConfig,
|
||||
normalizeWorldLabsKeyPoolConfig,
|
||||
WORLDLABS_KEY_POOL_CONFIG_KEY,
|
||||
type WorldLabsKeyPoolConfig,
|
||||
};
|
||||
|
||||
export const defaultWorldLabsKeyPoolConfig: WorldLabsKeyPoolConfig = {
|
||||
importToken: "",
|
||||
};
|
||||
|
||||
function normalizeImportToken(value: unknown) {
|
||||
return String(value || "")
|
||||
.replace(/[\u2028\u2029]/g, "")
|
||||
.replace(/[\u0000-\u001F\u007F]/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function normalizeWorldLabsKeyPoolConfig(
|
||||
value: unknown
|
||||
): WorldLabsKeyPoolConfig {
|
||||
if (!value || typeof value !== "object") {
|
||||
return defaultWorldLabsKeyPoolConfig;
|
||||
}
|
||||
|
||||
const raw = value as Partial<WorldLabsKeyPoolConfig>;
|
||||
return {
|
||||
importToken: normalizeImportToken(raw.importToken),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getWorldLabsKeyPoolConfig(): Promise<WorldLabsKeyPoolConfig> {
|
||||
try {
|
||||
const setting = await getSystemSetting({
|
||||
|
||||
60
lib/worldlabs/splat-utils.ts
Normal file
60
lib/worldlabs/splat-utils.ts
Normal file
@ -0,0 +1,60 @@
|
||||
function normalizeSplatLabel(label: string) {
|
||||
return String(label || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function parseResolutionScore(label: string) {
|
||||
const normalized = normalizeSplatLabel(label);
|
||||
if (!normalized) return 0;
|
||||
|
||||
if (
|
||||
normalized.includes("full") ||
|
||||
normalized.includes("origin") ||
|
||||
normalized.includes("source") ||
|
||||
normalized.includes("max")
|
||||
) {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
const match = normalized.match(/(\d+(?:\.\d+)?)\s*([km]?)/);
|
||||
if (!match) return 0;
|
||||
|
||||
const value = Number(match[1]);
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
|
||||
const unit = match[2];
|
||||
if (unit === "m") return Math.round(value * 1_000_000);
|
||||
if (unit === "k") return Math.round(value * 1_000);
|
||||
return Math.round(value);
|
||||
}
|
||||
|
||||
function compareSplatEntries(
|
||||
a: [string, string],
|
||||
b: [string, string]
|
||||
) {
|
||||
const scoreDelta = parseResolutionScore(b[0]) - parseResolutionScore(a[0]);
|
||||
if (scoreDelta !== 0) return scoreDelta;
|
||||
return a[0].localeCompare(b[0], "en");
|
||||
}
|
||||
|
||||
export function getSortedWorldLabsSplatEntries(
|
||||
spzUrls?: Record<string, string> | null
|
||||
) {
|
||||
return Object.entries(spzUrls || {})
|
||||
.filter(
|
||||
(entry): entry is [string, string] =>
|
||||
Boolean(entry[0]) && typeof entry[1] === "string" && entry[1].trim().length > 0
|
||||
)
|
||||
.sort(compareSplatEntries);
|
||||
}
|
||||
|
||||
export function pickBestWorldLabsSplatEntry(
|
||||
spzUrls?: Record<string, string> | null
|
||||
) {
|
||||
return getSortedWorldLabsSplatEntries(spzUrls)[0] || null;
|
||||
}
|
||||
|
||||
export function pickBestWorldLabsSplatUrl(
|
||||
spzUrls?: Record<string, string> | null
|
||||
) {
|
||||
return pickBestWorldLabsSplatEntry(spzUrls)?.[1] || null;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user