ai-chatbot/lib/tripo/client.ts
Your Name f6979f0196
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: 完善无限画布3D生成与实时画布能力
2026-02-28 22:33:16 +08:00

174 lines
4.6 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 { TripoApiEnvelope, TripoProviderConfig } from "./types";
const DEFAULT_TRIPO_BASE_URL = "https://api.tripo3d.ai/v2/openapi";
type TripoRequestOptions = {
method?: "GET" | "POST";
path: string;
json?: Record<string, unknown>;
formData?: FormData;
headers?: Record<string, string>;
};
export class TripoApiError extends Error {
code: number | null;
suggestion: string | null;
httpStatus: number;
traceId: string | null;
payload: unknown;
constructor(params: {
message: string;
code?: number | null;
suggestion?: string | null;
httpStatus: number;
traceId?: string | null;
payload?: unknown;
}) {
super(params.message);
this.name = "TripoApiError";
this.code = params.code ?? null;
this.suggestion = params.suggestion ?? null;
this.httpStatus = params.httpStatus;
this.traceId = params.traceId ?? null;
this.payload = params.payload;
}
}
function normalizeBaseUrl(baseUrl?: string) {
const resolved = String(baseUrl || "").trim() || DEFAULT_TRIPO_BASE_URL;
return resolved.replace(/\/+$/, "");
}
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;
}
export async function tripoRequest<T = unknown>(
config: TripoProviderConfig,
options: TripoRequestOptions
): Promise<{ data: T; traceId: string | null; raw: TripoApiEnvelope<T> | unknown }> {
const method = options.method || "GET";
const path = options.path.startsWith("/") ? options.path : `/${options.path}`;
const url = `${normalizeBaseUrl(config.baseUrl)}${path}`;
const headers = new Headers(options.headers || {});
headers.set("Authorization", `Bearer ${config.apiKey}`);
headers.set("Accept", "application/json");
let body: BodyInit | undefined;
if (options.formData) {
body = options.formData;
} else if (options.json) {
headers.set("Content-Type", "application/json");
body = JSON.stringify(options.json);
}
const response = await fetch(url, {
method,
headers,
body,
});
const traceId = response.headers.get("x-tripo-trace-id");
const parsed = await parseResponseBody(response);
const envelope =
parsed && typeof parsed === "object" && "code" in (parsed as Record<string, unknown>)
? (parsed as TripoApiEnvelope<T>)
: null;
if (!response.ok) {
const message =
envelope?.message ||
(typeof parsed === "string" ? parsed : `Tripo 请求失败(HTTP ${response.status})`);
throw new TripoApiError({
message,
code: envelope?.code ?? null,
suggestion: envelope?.suggestion ?? null,
httpStatus: response.status,
traceId,
payload: parsed,
});
}
if (!envelope) {
throw new TripoApiError({
message: "Tripo 响应格式异常",
httpStatus: response.status,
traceId,
payload: parsed,
});
}
if (envelope.code !== 0) {
throw new TripoApiError({
message: envelope.message || "Tripo 请求失败",
code: envelope.code,
suggestion: envelope.suggestion || null,
httpStatus: response.status,
traceId,
payload: envelope,
});
}
return {
data: envelope.data,
traceId,
raw: envelope,
};
}
export function pickTaskModelUrl(output?: Record<string, unknown> | null): string | null {
if (!output) return null;
const candidates = [
output.pbr_model,
output.model,
output.base_model,
output.rigged_model,
output.animated_model,
output.low_poly_model,
output.result_model,
output.glb,
output.fbx,
output.usdz,
output.obj,
output.stl,
];
for (const item of candidates) {
if (typeof item === "string") {
const value = item.trim();
if (value) return value;
continue;
}
if (item && typeof item === "object") {
const nested =
typeof (item as Record<string, unknown>).url === "string"
? String((item as Record<string, unknown>).url).trim()
: "";
if (nested) return nested;
}
}
return null;
}
export function pickTaskPreviewImageUrl(output?: Record<string, unknown> | null): string | null {
if (!output) return null;
const candidates = [
output.generated_image,
output.rendered_image,
output.preview_image,
output.thumbnail,
];
for (const item of candidates) {
const value = typeof item === "string" ? item.trim() : "";
if (value) return value;
}
return null;
}