feat: add worldlabs key pool import api
This commit is contained in:
parent
df1b1a017e
commit
078a115cfd
@ -195,7 +195,7 @@ interface RouteParams {
|
|||||||
params: Promise<{ taskId: string }>;
|
params: Promise<{ taskId: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(_request: Request, { params }: RouteParams) {
|
export async function GET(request: Request, { params }: RouteParams) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
@ -208,7 +208,8 @@ export async function GET(_request: Request, { params }: RouteParams) {
|
|||||||
return NextResponse.json({ success: false, error: "缺少 taskId" }, { status: 400 });
|
return NextResponse.json({ success: false, error: "缺少 taskId" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getWorldLabsProviderConfig();
|
const providerId = new URL(request.url).searchParams.get("providerId");
|
||||||
|
const config = await getWorldLabsProviderConfig(providerId);
|
||||||
const operation = await worldLabsRequest<WorldLabsOperation>(config, {
|
const operation = await worldLabsRequest<WorldLabsOperation>(config, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: `/marble/v1/operations/${encodeURIComponent(normalizedTaskId)}`,
|
path: `/marble/v1/operations/${encodeURIComponent(normalizedTaskId)}`,
|
||||||
@ -224,6 +225,8 @@ export async function GET(_request: Request, { params }: RouteParams) {
|
|||||||
progress: null,
|
progress: null,
|
||||||
output: operation.response ?? null,
|
output: operation.response ?? null,
|
||||||
assetPayload: {
|
assetPayload: {
|
||||||
|
providerId: config.providerId || null,
|
||||||
|
providerName: config.providerName || null,
|
||||||
operationMetadata: operation.metadata ?? null,
|
operationMetadata: operation.metadata ?? null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -291,7 +294,11 @@ export async function GET(_request: Request, { params }: RouteParams) {
|
|||||||
fallbackExtension: "spz",
|
fallbackExtension: "spz",
|
||||||
});
|
});
|
||||||
|
|
||||||
const assetPayload = buildAssetPayload(world, assets);
|
const assetPayload = {
|
||||||
|
providerId: config.providerId || null,
|
||||||
|
providerName: config.providerName || null,
|
||||||
|
...buildAssetPayload(world, assets),
|
||||||
|
};
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@ -6,7 +6,11 @@ import {
|
|||||||
getUserById,
|
getUserById,
|
||||||
} from "@/lib/db/queries";
|
} from "@/lib/db/queries";
|
||||||
import { WorldLabsApiError, worldLabsRequest } from "@/lib/worldlabs/client";
|
import { WorldLabsApiError, worldLabsRequest } from "@/lib/worldlabs/client";
|
||||||
import { getWorldLabsProviderConfig } from "@/lib/worldlabs/server";
|
import {
|
||||||
|
disableWorldLabsProvider,
|
||||||
|
getWorldLabsProviderConfig,
|
||||||
|
getWorldLabsProviderConfigs,
|
||||||
|
} from "@/lib/worldlabs/server";
|
||||||
import type { WorldLabsModel, WorldLabsOperation } from "@/lib/worldlabs/types";
|
import type { WorldLabsModel, WorldLabsOperation } from "@/lib/worldlabs/types";
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
@ -16,6 +20,7 @@ type WorldTaskMode = "text" | "image" | "multi-image" | "video";
|
|||||||
|
|
||||||
type CreateWorldTaskRequest = {
|
type CreateWorldTaskRequest = {
|
||||||
mode?: WorldTaskMode;
|
mode?: WorldTaskMode;
|
||||||
|
providerId?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
model?: WorldLabsModel | string;
|
model?: WorldLabsModel | string;
|
||||||
@ -128,6 +133,13 @@ function buildWorldPrompt(body: CreateWorldTaskRequest, mode: WorldTaskMode) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPermanentProviderError(error: unknown) {
|
||||||
|
return (
|
||||||
|
error instanceof WorldLabsApiError &&
|
||||||
|
[401, 402, 403].includes(error.httpStatus)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
@ -173,24 +185,61 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getWorldLabsProviderConfig();
|
const preferredProviderId = readOptionalTrimmedText(body.providerId);
|
||||||
const displayName =
|
const displayName =
|
||||||
readOptionalTrimmedText(body.displayName) ||
|
readOptionalTrimmedText(body.displayName) ||
|
||||||
readOptionalTrimmedText(body.prompt) ||
|
readOptionalTrimmedText(body.prompt) ||
|
||||||
`World ${new Date().toLocaleString("zh-CN")}`;
|
`World ${new Date().toLocaleString("zh-CN")}`;
|
||||||
const worldPrompt = buildWorldPrompt(body, mode);
|
const worldPrompt = buildWorldPrompt(body, mode);
|
||||||
const result = await worldLabsRequest<WorldLabsOperation>(config, {
|
|
||||||
method: "POST",
|
const candidates = preferredProviderId
|
||||||
path: "/marble/v1/worlds:generate",
|
? [await getWorldLabsProviderConfig(preferredProviderId)]
|
||||||
json: {
|
: await getWorldLabsProviderConfigs();
|
||||||
display_name: displayName,
|
if (candidates.length === 0) {
|
||||||
model: readOptionalTrimmedText(body.model) || "marble-1.1",
|
throw new Error("未找到可用的 World Labs Key");
|
||||||
permission: {
|
}
|
||||||
public: body.makePublic === true,
|
|
||||||
},
|
let result: WorldLabsOperation | null = null;
|
||||||
world_prompt: worldPrompt,
|
let usedProvider = null as Awaited<
|
||||||
},
|
ReturnType<typeof getWorldLabsProviderConfig>
|
||||||
});
|
> | null;
|
||||||
|
let lastError: unknown = null;
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
try {
|
||||||
|
result = await worldLabsRequest<WorldLabsOperation>(candidate, {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
usedProvider = candidate;
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
if (isPermanentProviderError(error)) {
|
||||||
|
await disableWorldLabsProvider(
|
||||||
|
candidate.providerId,
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (preferredProviderId) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result || !usedProvider) {
|
||||||
|
throw lastError instanceof Error
|
||||||
|
? lastError
|
||||||
|
: new Error("创建 World Labs 世界任务失败");
|
||||||
|
}
|
||||||
|
|
||||||
const taskId = String(result.operation_id || "").trim();
|
const taskId = String(result.operation_id || "").trim();
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
@ -223,6 +272,10 @@ export async function POST(request: Request) {
|
|||||||
data: {
|
data: {
|
||||||
taskId,
|
taskId,
|
||||||
status: result.done ? "success" : "running",
|
status: result.done ? "success" : "running",
|
||||||
|
assetPayload: {
|
||||||
|
providerId: usedProvider.providerId || null,
|
||||||
|
providerName: usedProvider.providerName || null,
|
||||||
|
},
|
||||||
raw: result,
|
raw: result,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
import { WorldLabsApiError, worldLabsRequest } from "@/lib/worldlabs/client";
|
import { WorldLabsApiError, worldLabsRequest } from "@/lib/worldlabs/client";
|
||||||
import { getWorldLabsProviderConfig } from "@/lib/worldlabs/server";
|
import {
|
||||||
|
disableWorldLabsProvider,
|
||||||
|
getWorldLabsProviderConfig,
|
||||||
|
getWorldLabsProviderConfigs,
|
||||||
|
} from "@/lib/worldlabs/server";
|
||||||
import type { WorldLabsPrepareUploadResponse } from "@/lib/worldlabs/types";
|
import type { WorldLabsPrepareUploadResponse } from "@/lib/worldlabs/types";
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
@ -41,6 +45,13 @@ function normalizeMediaAssetId(data: WorldLabsPrepareUploadResponse) {
|
|||||||
return String(data.media_asset?.media_asset_id || data.media_asset?.id || "").trim();
|
return String(data.media_asset?.media_asset_id || data.media_asset?.id || "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPermanentProviderError(error: unknown) {
|
||||||
|
return (
|
||||||
|
error instanceof WorldLabsApiError &&
|
||||||
|
[401, 402, 403].includes(error.httpStatus)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
@ -84,59 +95,91 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getWorldLabsProviderConfig();
|
const requestedProviderId = String(formData.get("providerId") || "").trim();
|
||||||
|
const candidates = requestedProviderId
|
||||||
|
? [await getWorldLabsProviderConfig(requestedProviderId)]
|
||||||
|
: await getWorldLabsProviderConfigs();
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
throw new Error("未找到可用的 World Labs Key");
|
||||||
|
}
|
||||||
|
|
||||||
const extension = guessExtension(file);
|
const extension = guessExtension(file);
|
||||||
const prepareUpload = await worldLabsRequest<WorldLabsPrepareUploadResponse>(config, {
|
let lastError: unknown = null;
|
||||||
method: "POST",
|
|
||||||
path: "/marble/v1/media-assets:prepare_upload",
|
|
||||||
json: {
|
|
||||||
file_name: file.name || `upload.${extension}`,
|
|
||||||
extension,
|
|
||||||
kind: isImage ? "image" : "video",
|
|
||||||
metadata: {
|
|
||||||
mime_type: file.type || null,
|
|
||||||
uploaded_from: "canvas_world_mode",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mediaAssetId = normalizeMediaAssetId(prepareUpload);
|
for (const config of candidates) {
|
||||||
const uploadUrl = String(prepareUpload.upload_info?.upload_url || "").trim();
|
try {
|
||||||
if (!mediaAssetId || !uploadUrl) {
|
const prepareUpload = await worldLabsRequest<WorldLabsPrepareUploadResponse>(
|
||||||
throw new Error("World Labs 返回的上传信息不完整");
|
config,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: "/marble/v1/media-assets:prepare_upload",
|
||||||
|
json: {
|
||||||
|
file_name: file.name || `upload.${extension}`,
|
||||||
|
extension,
|
||||||
|
kind: isImage ? "image" : "video",
|
||||||
|
metadata: {
|
||||||
|
mime_type: file.type || null,
|
||||||
|
uploaded_from: "canvas_world_mode",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const mediaAssetId = normalizeMediaAssetId(prepareUpload);
|
||||||
|
const uploadUrl = String(prepareUpload.upload_info?.upload_url || "").trim();
|
||||||
|
if (!mediaAssetId || !uploadUrl) {
|
||||||
|
throw new Error("World Labs 返回的上传信息不完整");
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadHeaders = new Headers();
|
||||||
|
uploadHeaders.set("Content-Type", file.type || "application/octet-stream");
|
||||||
|
const requiredHeaders = prepareUpload.upload_info?.required_headers || {};
|
||||||
|
for (const [headerName, headerValue] of Object.entries(requiredHeaders)) {
|
||||||
|
if (!headerName || headerValue === undefined || headerValue === null) continue;
|
||||||
|
uploadHeaders.set(headerName, String(headerValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadMethod = String(
|
||||||
|
prepareUpload.upload_info?.upload_method || "PUT"
|
||||||
|
).toUpperCase();
|
||||||
|
const uploadResponse = await fetch(uploadUrl, {
|
||||||
|
method: uploadMethod === "PUT" ? "PUT" : uploadMethod,
|
||||||
|
headers: uploadHeaders,
|
||||||
|
body: file,
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!uploadResponse.ok) {
|
||||||
|
const uploadError = await uploadResponse.text().catch(() => "");
|
||||||
|
throw new Error(
|
||||||
|
uploadError || `World Labs 文件上传失败(HTTP ${uploadResponse.status})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
mediaAssetId,
|
||||||
|
mediaAsset: prepareUpload.media_asset,
|
||||||
|
uploadInfo: prepareUpload.upload_info,
|
||||||
|
providerId: config.providerId || null,
|
||||||
|
providerName: config.providerName || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
if (isPermanentProviderError(error)) {
|
||||||
|
await disableWorldLabsProvider(config.providerId, (error as Error).message);
|
||||||
|
}
|
||||||
|
if (requestedProviderId) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadHeaders = new Headers();
|
throw lastError instanceof Error
|
||||||
uploadHeaders.set("Content-Type", file.type || "application/octet-stream");
|
? lastError
|
||||||
const requiredHeaders = prepareUpload.upload_info?.required_headers || {};
|
: new Error("上传 World Labs 媒体失败");
|
||||||
for (const [headerName, headerValue] of Object.entries(requiredHeaders)) {
|
|
||||||
if (!headerName || headerValue === undefined || headerValue === null) continue;
|
|
||||||
uploadHeaders.set(headerName, String(headerValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadMethod = String(prepareUpload.upload_info?.upload_method || "PUT").toUpperCase();
|
|
||||||
const uploadResponse = await fetch(uploadUrl, {
|
|
||||||
method: uploadMethod === "PUT" ? "PUT" : uploadMethod,
|
|
||||||
headers: uploadHeaders,
|
|
||||||
body: file,
|
|
||||||
cache: "no-store",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!uploadResponse.ok) {
|
|
||||||
const uploadError = await uploadResponse.text().catch(() => "");
|
|
||||||
throw new Error(
|
|
||||||
uploadError || `World Labs 文件上传失败(HTTP ${uploadResponse.status})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
mediaAssetId,
|
|
||||||
mediaAsset: prepareUpload.media_asset,
|
|
||||||
uploadInfo: prepareUpload.upload_info,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof WorldLabsApiError) {
|
if (error instanceof WorldLabsApiError) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
237
app/api/worldlabs/key-pool/import/route.ts
Normal file
237
app/api/worldlabs/key-pool/import/route.ts
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
createAiProvider,
|
||||||
|
getAiProvidersByType,
|
||||||
|
} from "@/lib/db/queries";
|
||||||
|
import { ChatSDKError } from "@/lib/errors";
|
||||||
|
import {
|
||||||
|
sanitizeWorldLabsApiKey,
|
||||||
|
sanitizeWorldLabsBaseUrl,
|
||||||
|
} from "@/lib/worldlabs/server";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
export const maxDuration = 60;
|
||||||
|
|
||||||
|
const DEFAULT_WORLDLABS_BASE_URL = "https://api.worldlabs.ai";
|
||||||
|
|
||||||
|
const importPayloadSchema = z.object({
|
||||||
|
keys: z
|
||||||
|
.array(
|
||||||
|
z.union([
|
||||||
|
z.string().min(1),
|
||||||
|
z.object({
|
||||||
|
apiKey: z.string().min(1),
|
||||||
|
name: z.string().min(1).max(128).optional(),
|
||||||
|
baseUrl: z.string().min(1).optional(),
|
||||||
|
isEnabled: z.boolean().optional(),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.max(5000)
|
||||||
|
.optional(),
|
||||||
|
apiKeysText: z.string().optional(),
|
||||||
|
namePrefix: z.string().min(1).max(96).optional(),
|
||||||
|
baseUrl: z.string().min(1).optional(),
|
||||||
|
isEnabled: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
function readImportToken(request: Request) {
|
||||||
|
const authHeader = request.headers.get("authorization") || "";
|
||||||
|
const bearerToken = authHeader.match(/^Bearer\s+(.+)$/i)?.[1]?.trim();
|
||||||
|
const customToken =
|
||||||
|
request.headers.get("x-worldlabs-import-token")?.trim() || "";
|
||||||
|
return bearerToken || customToken || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfiguredImportToken() {
|
||||||
|
return String(
|
||||||
|
process.env.WORLDLABS_KEY_POOL_IMPORT_TOKEN ||
|
||||||
|
process.env.WORLDLABS_IMPORT_TOKEN ||
|
||||||
|
""
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeImportItems(payload: z.infer<typeof importPayloadSchema>) {
|
||||||
|
const items: Array<{
|
||||||
|
apiKey: string;
|
||||||
|
name?: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
isEnabled?: boolean;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const item of payload.keys || []) {
|
||||||
|
if (typeof item === "string") {
|
||||||
|
items.push({ apiKey: item });
|
||||||
|
} else {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = String(payload.apiKeysText || "")
|
||||||
|
.split(/\r?\n/g)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
for (const line of lines) {
|
||||||
|
items.push({ apiKey: line });
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProviderName(params: {
|
||||||
|
namePrefix: string;
|
||||||
|
index: number;
|
||||||
|
overrideName?: string;
|
||||||
|
}) {
|
||||||
|
const customName = String(params.overrideName || "").trim();
|
||||||
|
if (customName) return customName.slice(0, 128);
|
||||||
|
|
||||||
|
const datePart = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.replace(/[-:TZ.]/g, "")
|
||||||
|
.slice(0, 14);
|
||||||
|
return `${params.namePrefix}-${datePart}-${String(params.index + 1).padStart(4, "0")}`.slice(
|
||||||
|
0,
|
||||||
|
128
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskKey(value: string) {
|
||||||
|
if (value.length <= 10) {
|
||||||
|
return `${value.slice(0, 2)}****`;
|
||||||
|
}
|
||||||
|
return `${value.slice(0, 4)}****${value.slice(-4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const configuredToken = getConfiguredImportToken();
|
||||||
|
if (!configuredToken) {
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
"服务端未配置 WORLDLABS_KEY_POOL_IMPORT_TOKEN,无法使用专用导入接口",
|
||||||
|
},
|
||||||
|
{ status: 503 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestToken = readImportToken(request);
|
||||||
|
if (!requestToken || requestToken !== configuredToken) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, error: "导入令牌无效" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawBody = await request.json().catch(() => ({}));
|
||||||
|
const payload = importPayloadSchema.parse(rawBody);
|
||||||
|
const inputItems = normalizeImportItems(payload);
|
||||||
|
|
||||||
|
if (inputItems.length === 0) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, error: "请至少提供一个 World Labs API Key" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingProviders = await getAiProvidersByType({
|
||||||
|
type: "worldlabs-3d",
|
||||||
|
enabledOnly: false,
|
||||||
|
});
|
||||||
|
const existingKeySet = new Set(
|
||||||
|
existingProviders
|
||||||
|
.map((provider) => sanitizeWorldLabsApiKey(String(provider.apiKey || "")))
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultBaseUrl =
|
||||||
|
sanitizeWorldLabsBaseUrl(payload.baseUrl || "") ||
|
||||||
|
DEFAULT_WORLDLABS_BASE_URL;
|
||||||
|
const defaultIsEnabled = payload.isEnabled !== false;
|
||||||
|
const namePrefix = String(payload.namePrefix || "WorldLabsKey").trim();
|
||||||
|
|
||||||
|
const dedupedImports: Array<{
|
||||||
|
apiKey: string;
|
||||||
|
name?: string;
|
||||||
|
baseUrl: string;
|
||||||
|
isEnabled: boolean;
|
||||||
|
}> = [];
|
||||||
|
const requestKeySet = new Set<string>();
|
||||||
|
const skipped: Array<{ apiKey: string; reason: string }> = [];
|
||||||
|
|
||||||
|
for (const item of inputItems) {
|
||||||
|
const apiKey = sanitizeWorldLabsApiKey(item.apiKey);
|
||||||
|
if (!apiKey) {
|
||||||
|
skipped.push({ apiKey: "", reason: "empty_key" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (existingKeySet.has(apiKey)) {
|
||||||
|
skipped.push({ apiKey: maskKey(apiKey), reason: "duplicate_existing" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (requestKeySet.has(apiKey)) {
|
||||||
|
skipped.push({ apiKey: maskKey(apiKey), reason: "duplicate_in_request" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestKeySet.add(apiKey);
|
||||||
|
dedupedImports.push({
|
||||||
|
apiKey,
|
||||||
|
name: item.name,
|
||||||
|
baseUrl:
|
||||||
|
sanitizeWorldLabsBaseUrl(item.baseUrl || "") || defaultBaseUrl,
|
||||||
|
isEnabled: item.isEnabled ?? defaultIsEnabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const imported = [];
|
||||||
|
for (const [index, item] of dedupedImports.entries()) {
|
||||||
|
const created = await createAiProvider({
|
||||||
|
name: buildProviderName({
|
||||||
|
namePrefix,
|
||||||
|
index,
|
||||||
|
overrideName: item.name,
|
||||||
|
}),
|
||||||
|
type: "worldlabs-3d",
|
||||||
|
baseUrl: item.baseUrl,
|
||||||
|
apiKey: item.apiKey,
|
||||||
|
isEnabled: item.isEnabled,
|
||||||
|
});
|
||||||
|
imported.push({
|
||||||
|
id: created[0]?.id,
|
||||||
|
name: created[0]?.name,
|
||||||
|
isEnabled: created[0]?.isEnabled === true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
importedCount: imported.length,
|
||||||
|
skippedCount: skipped.length,
|
||||||
|
imported,
|
||||||
|
skipped,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ChatSDKError) {
|
||||||
|
return error.toResponse();
|
||||||
|
}
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, error: error.flatten() },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
error instanceof Error ? error.message : "导入 World Labs key 失败",
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1027,7 +1027,9 @@ export default function CanvasModel3DPage() {
|
|||||||
const isWorldTask = currentTask.provider === "worldlabs-3d";
|
const isWorldTask = currentTask.provider === "worldlabs-3d";
|
||||||
const result =
|
const result =
|
||||||
isWorldTask
|
isWorldTask
|
||||||
? await worldLabsGenerationService.getTask(currentTask.taskId)
|
? await worldLabsGenerationService.getTask(currentTask.taskId, {
|
||||||
|
providerId: currentTask.assetPayload?.providerId,
|
||||||
|
})
|
||||||
: await model3DGenerationService.getTask(currentTask.taskId);
|
: await model3DGenerationService.getTask(currentTask.taskId);
|
||||||
if (!result.success || !result.data) {
|
if (!result.success || !result.data) {
|
||||||
const failMessage = result.error || "任务查询失败";
|
const failMessage = result.error || "任务查询失败";
|
||||||
@ -1246,6 +1248,11 @@ export default function CanvasModel3DPage() {
|
|||||||
if (!createResult.success || !createResult.data?.taskId) {
|
if (!createResult.success || !createResult.data?.taskId) {
|
||||||
throw new Error(createResult.error || "创建任务失败");
|
throw new Error(createResult.error || "创建任务失败");
|
||||||
}
|
}
|
||||||
|
const worldCreateResultData =
|
||||||
|
params.provider === "worldlabs-3d" &&
|
||||||
|
"assetPayload" in createResult.data
|
||||||
|
? createResult.data
|
||||||
|
: null;
|
||||||
|
|
||||||
const newTask: TaskRecord = {
|
const newTask: TaskRecord = {
|
||||||
taskId: createResult.data.taskId,
|
taskId: createResult.data.taskId,
|
||||||
@ -1257,7 +1264,10 @@ export default function CanvasModel3DPage() {
|
|||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
sourceTaskId: params.sourceTaskId || null,
|
sourceTaskId: params.sourceTaskId || null,
|
||||||
exportFormat: params.exportFormat || null,
|
exportFormat: params.exportFormat || null,
|
||||||
assetPayload: params.assetPayload ?? null,
|
assetPayload:
|
||||||
|
params.provider === "worldlabs-3d"
|
||||||
|
? worldCreateResultData?.assetPayload ?? params.assetPayload ?? null
|
||||||
|
: params.assetPayload ?? null,
|
||||||
};
|
};
|
||||||
setCurrentTask(newTask);
|
setCurrentTask(newTask);
|
||||||
upsertTaskHistory(newTask);
|
upsertTaskHistory(newTask);
|
||||||
@ -1492,6 +1502,7 @@ export default function CanvasModel3DPage() {
|
|||||||
payload: {
|
payload: {
|
||||||
mode: "image",
|
mode: "image",
|
||||||
mediaAssetId: uploadResult.data.mediaAssetId,
|
mediaAssetId: uploadResult.data.mediaAssetId,
|
||||||
|
providerId: uploadResult.data.providerId,
|
||||||
prompt: normalizedPrompt || undefined,
|
prompt: normalizedPrompt || undefined,
|
||||||
displayName: worldDisplayName.trim() || undefined,
|
displayName: worldDisplayName.trim() || undefined,
|
||||||
model: worldModel,
|
model: worldModel,
|
||||||
@ -1521,6 +1532,7 @@ export default function CanvasModel3DPage() {
|
|||||||
payload: {
|
payload: {
|
||||||
mode: "video",
|
mode: "video",
|
||||||
mediaAssetId: uploadResult.data.mediaAssetId,
|
mediaAssetId: uploadResult.data.mediaAssetId,
|
||||||
|
providerId: uploadResult.data.providerId,
|
||||||
prompt: normalizedPrompt || undefined,
|
prompt: normalizedPrompt || undefined,
|
||||||
displayName: worldDisplayName.trim() || undefined,
|
displayName: worldDisplayName.trim() || undefined,
|
||||||
model: worldModel,
|
model: worldModel,
|
||||||
@ -1535,12 +1547,24 @@ export default function CanvasModel3DPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setUploadingImage(true);
|
setUploadingImage(true);
|
||||||
const uploads = await Promise.all(
|
let boundWorldProviderId: string | null = null;
|
||||||
selectedWorldImages.map(async (slot) => ({
|
const uploads: Array<{
|
||||||
slot,
|
slot: WorldImageSlot;
|
||||||
result: await worldLabsGenerationService.uploadMedia(slot.file as File, "image"),
|
result: Awaited<ReturnType<typeof worldLabsGenerationService.uploadMedia>>;
|
||||||
}))
|
}> = [];
|
||||||
);
|
for (const slot of selectedWorldImages) {
|
||||||
|
const result = await worldLabsGenerationService.uploadMedia(
|
||||||
|
slot.file as File,
|
||||||
|
"image",
|
||||||
|
{
|
||||||
|
providerId: boundWorldProviderId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (result.success && result.data?.providerId && !boundWorldProviderId) {
|
||||||
|
boundWorldProviderId = result.data.providerId;
|
||||||
|
}
|
||||||
|
uploads.push({ slot, result });
|
||||||
|
}
|
||||||
|
|
||||||
const mediaAssets = uploads.map(({ slot, result }) => {
|
const mediaAssets = uploads.map(({ slot, result }) => {
|
||||||
if (!result.success || !result.data?.mediaAssetId) {
|
if (!result.success || !result.data?.mediaAssetId) {
|
||||||
@ -1556,6 +1580,7 @@ export default function CanvasModel3DPage() {
|
|||||||
provider: "worldlabs-3d",
|
provider: "worldlabs-3d",
|
||||||
payload: {
|
payload: {
|
||||||
mode: "multi-image",
|
mode: "multi-image",
|
||||||
|
providerId: boundWorldProviderId,
|
||||||
mediaAssets,
|
mediaAssets,
|
||||||
prompt: normalizedPrompt || undefined,
|
prompt: normalizedPrompt || undefined,
|
||||||
displayName: worldDisplayName.trim() || undefined,
|
displayName: worldDisplayName.trim() || undefined,
|
||||||
|
|||||||
@ -3357,6 +3357,32 @@ export async function getEnabledAiProviders(): Promise<AiProvider[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAiProvidersByType({
|
||||||
|
type,
|
||||||
|
enabledOnly = false,
|
||||||
|
}: {
|
||||||
|
type: AiProvider["type"];
|
||||||
|
enabledOnly?: boolean;
|
||||||
|
}): Promise<AiProvider[]> {
|
||||||
|
try {
|
||||||
|
return await db
|
||||||
|
.select()
|
||||||
|
.from(aiProvider)
|
||||||
|
.where(
|
||||||
|
enabledOnly
|
||||||
|
? and(eq(aiProvider.type, type), eq(aiProvider.isEnabled, true))
|
||||||
|
: eq(aiProvider.type, type)
|
||||||
|
)
|
||||||
|
.orderBy(asc(aiProvider.createdAt));
|
||||||
|
} catch (_error) {
|
||||||
|
console.error("getAiProvidersByType error:", _error);
|
||||||
|
throw new ChatSDKError(
|
||||||
|
"bad_request:database",
|
||||||
|
typeof _error === "object" ? JSON.stringify(_error) : String(_error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAiProviderById({
|
export async function getAiProviderById({
|
||||||
id,
|
id,
|
||||||
}: {
|
}: {
|
||||||
@ -3386,7 +3412,7 @@ export async function createAiProvider({
|
|||||||
updatedBy,
|
updatedBy,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "runway";
|
type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway";
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
isEnabled?: boolean;
|
isEnabled?: boolean;
|
||||||
@ -3424,7 +3450,7 @@ export async function updateAiProvider({
|
|||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
type?: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "runway";
|
type?: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway";
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
isEnabled?: boolean;
|
isEnabled?: boolean;
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
|
|
||||||
import { getEnabledAiProviders } from "@/lib/db/queries";
|
import {
|
||||||
|
getAiProvidersByType,
|
||||||
|
updateAiProvider,
|
||||||
|
} from "@/lib/db/queries";
|
||||||
import type { WorldLabsProviderConfig } from "./types";
|
import type { WorldLabsProviderConfig } from "./types";
|
||||||
|
|
||||||
const DEFAULT_WORLDLABS_BASE_URL = "https://api.worldlabs.ai";
|
const DEFAULT_WORLDLABS_BASE_URL = "https://api.worldlabs.ai";
|
||||||
|
|
||||||
function sanitizeWorldLabsApiKey(value: string): string {
|
export function sanitizeWorldLabsApiKey(value: string): string {
|
||||||
return String(value || "")
|
return String(value || "")
|
||||||
.replace(/[\u2028\u2029]/g, "")
|
.replace(/[\u2028\u2029]/g, "")
|
||||||
.replace(/[\u0000-\u001F\u007F]/g, "")
|
.replace(/[\u0000-\u001F\u007F]/g, "")
|
||||||
@ -13,38 +16,96 @@ function sanitizeWorldLabsApiKey(value: string): string {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeWorldLabsBaseUrl(value: string): string {
|
export function sanitizeWorldLabsBaseUrl(value: string): string {
|
||||||
return String(value || "")
|
return String(value || "")
|
||||||
.replace(/[\u2028\u2029]/g, "")
|
.replace(/[\u2028\u2029]/g, "")
|
||||||
.replace(/[\u0000-\u001F\u007F]/g, "")
|
.replace(/[\u0000-\u001F\u007F]/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWorldLabsProviderConfig(): Promise<WorldLabsProviderConfig> {
|
export async function getWorldLabsProviderConfigs(): Promise<
|
||||||
const providers = await getEnabledAiProviders();
|
WorldLabsProviderConfig[]
|
||||||
const worldLabsProvider = providers.find((item) => item.type === "worldlabs-3d");
|
> {
|
||||||
|
const providers = await getAiProvidersByType({
|
||||||
|
type: "worldlabs-3d",
|
||||||
|
enabledOnly: true,
|
||||||
|
});
|
||||||
|
|
||||||
const providerApiKey = sanitizeWorldLabsApiKey(String(worldLabsProvider?.apiKey || ""));
|
const normalizedProviders = providers
|
||||||
if (worldLabsProvider && providerApiKey) {
|
.map((provider): WorldLabsProviderConfig | null => {
|
||||||
return {
|
const providerApiKey = sanitizeWorldLabsApiKey(
|
||||||
apiKey: providerApiKey,
|
String(provider.apiKey || "")
|
||||||
baseUrl:
|
);
|
||||||
sanitizeWorldLabsBaseUrl(String(worldLabsProvider?.baseUrl || "")) ||
|
if (!providerApiKey) return null;
|
||||||
DEFAULT_WORLDLABS_BASE_URL,
|
return {
|
||||||
providerId: worldLabsProvider.id,
|
apiKey: providerApiKey,
|
||||||
providerName: worldLabsProvider.name,
|
baseUrl:
|
||||||
};
|
sanitizeWorldLabsBaseUrl(String(provider.baseUrl || "")) ||
|
||||||
|
DEFAULT_WORLDLABS_BASE_URL,
|
||||||
|
providerId: provider.id,
|
||||||
|
providerName: provider.name,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item): item is WorldLabsProviderConfig => Boolean(item));
|
||||||
|
|
||||||
|
if (normalizedProviders.length > 0) {
|
||||||
|
return normalizedProviders;
|
||||||
}
|
}
|
||||||
|
|
||||||
const envApiKey = sanitizeWorldLabsApiKey(String(process.env.WORLDLABS_API_KEY || ""));
|
const envApiKey = sanitizeWorldLabsApiKey(String(process.env.WORLDLABS_API_KEY || ""));
|
||||||
if (!envApiKey) {
|
if (!envApiKey) {
|
||||||
throw new Error("未配置 World Labs API,请在系统设置-本地AI新增 World Labs 提供商");
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return [
|
||||||
apiKey: envApiKey,
|
{
|
||||||
baseUrl:
|
apiKey: envApiKey,
|
||||||
sanitizeWorldLabsBaseUrl(String(process.env.WORLDLABS_BASE_URL || "")) ||
|
baseUrl:
|
||||||
DEFAULT_WORLDLABS_BASE_URL,
|
sanitizeWorldLabsBaseUrl(String(process.env.WORLDLABS_BASE_URL || "")) ||
|
||||||
};
|
DEFAULT_WORLDLABS_BASE_URL,
|
||||||
|
providerName: "WORLDLABS_API_KEY",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWorldLabsProviderConfig(
|
||||||
|
preferredProviderId?: string | null
|
||||||
|
): Promise<WorldLabsProviderConfig> {
|
||||||
|
const providers = await getWorldLabsProviderConfigs();
|
||||||
|
const preferredId = String(preferredProviderId || "").trim();
|
||||||
|
const worldLabsProvider = preferredId
|
||||||
|
? providers.find((item) => item.providerId === preferredId) || null
|
||||||
|
: providers[0] || null;
|
||||||
|
|
||||||
|
if (worldLabsProvider) {
|
||||||
|
return worldLabsProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("未配置 World Labs API,请在系统设置-本地AI新增 World Labs 提供商");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function disableWorldLabsProvider(
|
||||||
|
providerId?: string | null,
|
||||||
|
reason?: string
|
||||||
|
) {
|
||||||
|
const normalizedProviderId = String(providerId || "").trim();
|
||||||
|
if (!normalizedProviderId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateAiProvider({
|
||||||
|
id: normalizedProviderId,
|
||||||
|
isEnabled: false,
|
||||||
|
});
|
||||||
|
console.warn(
|
||||||
|
`[WorldLabsPool] Disabled provider ${normalizedProviderId}${
|
||||||
|
reason ? `: ${reason}` : ""
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[WorldLabsPool] Failed to disable provider:", {
|
||||||
|
providerId: normalizedProviderId,
|
||||||
|
reason,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
proxy.ts
5
proxy.ts
@ -251,6 +251,11 @@ export async function proxy(request: NextRequest) {
|
|||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 允许 World Labs key 池导入接口由外部脚本调用(具体鉴权由接口内 token 负责)
|
||||||
|
if (pathname === "/api/worldlabs/key-pool/import") {
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
const token = await resolveToken();
|
const token = await resolveToken();
|
||||||
|
|
||||||
const isPublicPage =
|
const isPublicPage =
|
||||||
|
|||||||
@ -825,6 +825,8 @@ export const model3DGenerationService = {
|
|||||||
export type WorldLabsTaskMode = 'text' | 'image' | 'multi-image' | 'video';
|
export type WorldLabsTaskMode = 'text' | 'image' | 'multi-image' | 'video';
|
||||||
|
|
||||||
export type WorldLabsTaskAssetPayload = {
|
export type WorldLabsTaskAssetPayload = {
|
||||||
|
providerId?: string | null;
|
||||||
|
providerName?: string | null;
|
||||||
worldId?: string | null;
|
worldId?: string | null;
|
||||||
worldUrl?: string | null;
|
worldUrl?: string | null;
|
||||||
displayName?: string | null;
|
displayName?: string | null;
|
||||||
@ -840,12 +842,24 @@ export type WorldLabsTaskAssetPayload = {
|
|||||||
export const worldLabsGenerationService = {
|
export const worldLabsGenerationService = {
|
||||||
async uploadMedia(
|
async uploadMedia(
|
||||||
file: File,
|
file: File,
|
||||||
kind: 'image' | 'video'
|
kind: 'image' | 'video',
|
||||||
): Promise<ApiResponse<{ mediaAssetId?: string; mediaAsset?: Record<string, any>; uploadInfo?: Record<string, any> }>> {
|
options?: { providerId?: string | null }
|
||||||
|
): Promise<
|
||||||
|
ApiResponse<{
|
||||||
|
mediaAssetId?: string;
|
||||||
|
mediaAsset?: Record<string, any>;
|
||||||
|
uploadInfo?: Record<string, any>;
|
||||||
|
providerId?: string | null;
|
||||||
|
providerName?: string | null;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('kind', kind);
|
formData.append('kind', kind);
|
||||||
|
if (options?.providerId) {
|
||||||
|
formData.append('providerId', options.providerId);
|
||||||
|
}
|
||||||
const response = await fetch('/api/canvas/world/upload', {
|
const response = await fetch('/api/canvas/world/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
@ -857,7 +871,14 @@ export const worldLabsGenerationService = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async createTask(params: Record<string, any>): Promise<ApiResponse<{ taskId: string; status: Model3DTaskStatus; raw?: any }>> {
|
async createTask(params: Record<string, any>): Promise<
|
||||||
|
ApiResponse<{
|
||||||
|
taskId: string;
|
||||||
|
status: Model3DTaskStatus;
|
||||||
|
raw?: any;
|
||||||
|
assetPayload?: WorldLabsTaskAssetPayload | null;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/canvas/world/tasks', {
|
const response = await fetch('/api/canvas/world/tasks', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -871,7 +892,10 @@ export const worldLabsGenerationService = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getTask(taskId: string): Promise<ApiResponse<{
|
async getTask(
|
||||||
|
taskId: string,
|
||||||
|
options?: { providerId?: string | null }
|
||||||
|
): Promise<ApiResponse<{
|
||||||
taskId: string;
|
taskId: string;
|
||||||
type: string;
|
type: string;
|
||||||
status: Model3DTaskStatus;
|
status: Model3DTaskStatus;
|
||||||
@ -887,7 +911,11 @@ export const worldLabsGenerationService = {
|
|||||||
errorMessage?: string | null;
|
errorMessage?: string | null;
|
||||||
}>> {
|
}>> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/canvas/world/tasks/${taskId}`);
|
const url = new URL(`/api/canvas/world/tasks/${taskId}`, window.location.origin);
|
||||||
|
if (options?.providerId) {
|
||||||
|
url.searchParams.set('providerId', options.providerId);
|
||||||
|
}
|
||||||
|
const response = await fetch(url.toString());
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user