Your Name d76b31952f
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add world labs 3d workspace mode
2026-04-04 22:06:43 +08:00

118 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "server-only";
import type { WorldLabsProviderConfig } from "./types";
type WorldLabsRequestOptions = {
method?: "GET" | "POST";
path: string;
json?: Record<string, unknown>;
headers?: Record<string, string>;
};
const DEFAULT_WORLDLABS_BASE_URL = "https://api.worldlabs.ai";
export class WorldLabsApiError extends Error {
httpStatus: number;
payload: unknown;
constructor(params: {
message: string;
httpStatus: number;
payload?: unknown;
}) {
super(params.message);
this.name = "WorldLabsApiError";
this.httpStatus = params.httpStatus;
this.payload = params.payload;
}
}
function normalizeBaseUrl(baseUrl?: string) {
const resolved = String(baseUrl || "").trim() || DEFAULT_WORLDLABS_BASE_URL;
return resolved.replace(/\/+$/, "");
}
function sanitizeApiKey(value: string): string {
return String(value || "")
.replace(/[\u2028\u2029]/g, "")
.replace(/[\u0000-\u001F\u007F]/g, "")
.trim();
}
async function parseResponseBody(response: Response): Promise<unknown> {
const contentType = response.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
return response.json().catch(() => null);
}
const text = await response.text().catch(() => "");
return text || null;
}
function extractErrorMessage(payload: unknown, fallback: string) {
if (!payload) return fallback;
if (typeof payload === "string") return payload || fallback;
if (typeof payload !== "object") return fallback;
const record = payload as Record<string, unknown>;
if (typeof record.message === "string" && record.message.trim()) {
return record.message.trim();
}
if (typeof record.detail === "string" && record.detail.trim()) {
return record.detail.trim();
}
if (Array.isArray(record.detail) && record.detail.length > 0) {
const firstDetail = record.detail[0];
if (firstDetail && typeof firstDetail === "object") {
const msg = (firstDetail as Record<string, unknown>).msg;
if (typeof msg === "string" && msg.trim()) {
return msg.trim();
}
}
}
return fallback;
}
export async function worldLabsRequest<T = unknown>(
config: WorldLabsProviderConfig,
options: WorldLabsRequestOptions
): Promise<T> {
const path = options.path.startsWith("/") ? options.path : `/${options.path}`;
const url = `${normalizeBaseUrl(config.baseUrl)}${path}`;
const headers = new Headers(options.headers || {});
const normalizedApiKey = sanitizeApiKey(String(config.apiKey || "")).replace(/\s+/g, "");
if (!normalizedApiKey) {
throw new WorldLabsApiError({
message: "World Labs API Key 为空,请在本地AI设置中重新保存",
httpStatus: 400,
});
}
headers.set("WLT-Api-Key", normalizedApiKey);
headers.set("Accept", "application/json");
let body: BodyInit | undefined;
if (options.json) {
headers.set("Content-Type", "application/json");
body = JSON.stringify(options.json);
}
const response = await fetch(url, {
method: options.method || "GET",
headers,
body,
cache: "no-store",
});
const parsed = await parseResponseBody(response);
if (!response.ok) {
throw new WorldLabsApiError({
message: extractErrorMessage(parsed, `World Labs 请求失败(HTTP ${response.status})`),
httpStatus: response.status,
payload: parsed,
});
}
return parsed as T;
}