fix(worldlabs): increase world 3d api request timeouts

This commit is contained in:
Your Name 2026-06-09 13:32:24 +08:00
parent 74adb68d16
commit 6b90b25d5c
4 changed files with 90 additions and 16 deletions

View File

@ -98,6 +98,9 @@ WORLDLABS_API_KEY=
WORLDLABS_BASE_URL=
WORLDLABS_IMPORT_TOKEN=
WORLDLABS_KEY_POOL_IMPORT_TOKEN=
WORLDLABS_API_TIMEOUT_MS=180000
WORLDLABS_TASK_CREATE_TIMEOUT_MS=180000
WORLDLABS_TASK_STATUS_TIMEOUT_MS=180000
KITTYCAD_API_TOKEN=
KITTYCAD_API_KEY=
KITTYCAD_BASE_URL=https://api.zoo.dev

View File

@ -16,9 +16,18 @@ import type {
} from "@/lib/worldlabs/types";
export const runtime = "nodejs";
export const maxDuration = 120;
export const maxDuration = 180;
const logger = createLogger("world-labs-task-status");
const WORLDLABS_TASK_STATUS_TIMEOUT_MS =
Number.parseInt(String(process.env.WORLDLABS_TASK_STATUS_TIMEOUT_MS || "").trim(), 10) ||
180_000;
function resolveWorldLabsStatusTimeoutMs() {
if (!Number.isFinite(WORLDLABS_TASK_STATUS_TIMEOUT_MS)) return 180_000;
if (WORLDLABS_TASK_STATUS_TIMEOUT_MS <= 0) return 180_000;
return WORLDLABS_TASK_STATUS_TIMEOUT_MS;
}
type ArchiveCacheItem = {
sourceUrl: string;
@ -446,6 +455,7 @@ export async function GET(request: Request, { params }: RouteParams) {
const operation = await worldLabsRequest<WorldLabsOperation>(config, {
method: "GET",
path: `/marble/v1/operations/${encodeURIComponent(normalizedTaskId)}`,
timeoutMs: resolveWorldLabsStatusTimeoutMs(),
});
if (!operation.done) {
@ -505,6 +515,7 @@ export async function GET(request: Request, { params }: RouteParams) {
const detailedWorld = await worldLabsRequest<WorldLabsWorld>(config, {
method: "GET",
path: `/marble/v1/worlds/${encodeURIComponent(worldId)}`,
timeoutMs: resolveWorldLabsStatusTimeoutMs(),
});
world = world
? {

View File

@ -21,9 +21,18 @@ import {
import type { WorldLabsModel, WorldLabsOperation } from "@/lib/worldlabs/types";
export const runtime = "nodejs";
export const maxDuration = 60;
export const maxDuration = 180;
const logger = createLogger("world-labs-task-create");
const WORLDLABS_TASK_CREATE_TIMEOUT_MS =
Number.parseInt(String(process.env.WORLDLABS_TASK_CREATE_TIMEOUT_MS || "").trim(), 10) ||
180_000;
function resolveWorldLabsCreateTimeoutMs() {
if (!Number.isFinite(WORLDLABS_TASK_CREATE_TIMEOUT_MS)) return 180_000;
if (WORLDLABS_TASK_CREATE_TIMEOUT_MS <= 0) return 180_000;
return WORLDLABS_TASK_CREATE_TIMEOUT_MS;
}
type WorldTaskMode = "text" | "image" | "multi-image" | "video";
@ -236,6 +245,7 @@ export async function POST(request: Request) {
result = await worldLabsRequest<WorldLabsOperation>(candidate, {
method: "POST",
path: "/marble/v1/worlds:generate",
timeoutMs: resolveWorldLabsCreateTimeoutMs(),
json: {
display_name: displayName,
model: resolvedWorldModel || "marble-1.1",

View File

@ -7,9 +7,43 @@ type WorldLabsRequestOptions = {
path: string;
json?: Record<string, unknown>;
headers?: Record<string, string>;
timeoutMs?: number;
};
const DEFAULT_WORLDLABS_BASE_URL = "https://api.worldlabs.ai";
const DEFAULT_WORLDLABS_REQUEST_TIMEOUT_MS = 180_000;
function toIntMs(value: string | undefined) {
const parsed = Number.parseInt(String(value || "").trim(), 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
return null;
}
return parsed;
}
function getDefaultWorldLabsTimeoutMs() {
return toIntMs(process.env.WORLDLABS_API_TIMEOUT_MS) || DEFAULT_WORLDLABS_REQUEST_TIMEOUT_MS;
}
function createRequestSignal(timeoutMs: number) {
const timeoutSignal = (globalThis as Record<string, unknown>).AbortSignal;
if (timeoutSignal && typeof (timeoutSignal as typeof AbortSignal).timeout === "function") {
return {
signal: (timeoutSignal as typeof AbortSignal).timeout(timeoutMs),
cancel: () => undefined,
};
}
const controller = new AbortController();
const timer = setTimeout(() => {
controller.abort(`World Labs 请求超时 (${timeoutMs}ms)`);
}, timeoutMs);
return {
signal: controller.signal,
cancel: () => clearTimeout(timer),
};
}
export class WorldLabsApiError extends Error {
httpStatus: number;
@ -97,21 +131,37 @@ export async function worldLabsRequest<T = unknown>(
body = JSON.stringify(options.json);
}
const response = await fetch(url, {
method: options.method || "GET",
headers,
body,
cache: "no-store",
});
const parsed = await parseResponseBody(response);
const timeoutMs = options.timeoutMs || getDefaultWorldLabsTimeoutMs();
const { signal, cancel } = createRequestSignal(timeoutMs);
if (!response.ok) {
throw new WorldLabsApiError({
message: extractErrorMessage(parsed, `World Labs 请求失败(HTTP ${response.status})`),
httpStatus: response.status,
payload: parsed,
try {
const response = await fetch(url, {
method: options.method || "GET",
headers,
body,
cache: "no-store",
signal,
});
}
const parsed = await parseResponseBody(response);
return parsed as T;
if (!response.ok) {
throw new WorldLabsApiError({
message: extractErrorMessage(parsed, `World Labs 请求失败(HTTP ${response.status})`),
httpStatus: response.status,
payload: parsed,
});
}
return parsed as T;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new WorldLabsApiError({
message: `World Labs 请求超时(${timeoutMs}ms)`,
httpStatus: 504,
});
}
throw error;
} finally {
cancel();
}
}