118 lines
3.3 KiB
TypeScript
118 lines
3.3 KiB
TypeScript
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;
|
||
}
|