import "server-only"; import type { KittyCadProviderConfig } from "./server"; type KittyCadRequestOptions = { method?: "GET" | "POST"; path: string; json?: Record; headers?: Record; }; const DEFAULT_KITTYCAD_BASE_URL = "https://api.zoo.dev"; const TRAILING_SLASH_RE = /\/+$/; export class KittyCadApiError extends Error { httpStatus: number; payload: unknown; constructor(params: { message: string; httpStatus: number; payload?: unknown; }) { super(params.message); this.name = "KittyCadApiError"; this.httpStatus = params.httpStatus; this.payload = params.payload; } } function normalizeBaseUrl(baseUrl?: string) { const resolved = String(baseUrl || "").trim() || DEFAULT_KITTYCAD_BASE_URL; return resolved.replace(TRAILING_SLASH_RE, ""); } function sanitizeApiKey(value: string): string { let result = ""; for (const char of String(value || "")) { const code = char.charCodeAt(0); if (code === 0x20_28 || code === 0x20_29) { continue; } if ((code >= 0 && code <= 0x1f) || code === 0x7f) { continue; } result += char; } return result.trim(); } async function parseResponseBody(response: Response): Promise { 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; if (typeof record.error === "string" && record.error.trim()) { return record.error.trim(); } if (typeof record.message === "string" && record.message.trim()) { return record.message.trim(); } if (typeof record.detail === "string" && record.detail.trim()) { return record.detail.trim(); } return fallback; } export async function kittyCadRequest( config: KittyCadProviderConfig, options: KittyCadRequestOptions ): Promise { 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 KittyCadApiError({ message: "KittyCAD API Token 为空,请在本地AI设置中重新保存", httpStatus: 400, }); } headers.set("Authorization", `Bearer ${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 KittyCadApiError({ message: extractErrorMessage( parsed, `KittyCAD 请求失败(HTTP ${response.status})` ), httpStatus: response.status, payload: parsed, }); } return parsed as T; }