168 lines
4.8 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>;
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;
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 timeoutMs = options.timeoutMs || getDefaultWorldLabsTimeoutMs();
const { signal, cancel } = createRequestSignal(timeoutMs);
try {
const response = await fetch(url, {
method: options.method || "GET",
headers,
body,
cache: "no-store",
signal,
});
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;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new WorldLabsApiError({
message: `World Labs 请求超时(${timeoutMs}ms)`,
httpStatus: 504,
});
}
throw error;
} finally {
cancel();
}
}