155 lines
4.5 KiB
TypeScript
155 lines
4.5 KiB
TypeScript
import "server-only";
|
||
|
||
import { getAiProvidersByType } from "@/lib/db/queries";
|
||
import { TripoApiError, tripoRequest } from "./client";
|
||
import type { TripoProviderConfig } from "./types";
|
||
|
||
const DEFAULT_TRIPO_BASE_URL = "https://api.tripo3d.ai/v2/openapi";
|
||
let tripoProviderCursor = 0;
|
||
|
||
function sanitizeTripoApiKey(value: string): string {
|
||
return String(value || "")
|
||
.replace(/[\u2028\u2029]/g, "")
|
||
.replace(/[\u0000-\u001F\u007F]/g, "")
|
||
.replace(/\s+/g, "")
|
||
.trim();
|
||
}
|
||
|
||
function sanitizeTripoBaseUrl(value: string): string {
|
||
return String(value || "")
|
||
.replace(/[\u2028\u2029]/g, "")
|
||
.replace(/[\u0000-\u001F\u007F]/g, "")
|
||
.trim();
|
||
}
|
||
|
||
function isTripoTaskNotFoundError(error: unknown): error is TripoApiError {
|
||
if (!(error instanceof TripoApiError)) return false;
|
||
if (error.httpStatus === 404) return true;
|
||
const normalizedMessage = String(error.message || "").toLowerCase();
|
||
return (
|
||
normalizedMessage.includes("task is not found") ||
|
||
normalizedMessage.includes("task not found")
|
||
);
|
||
}
|
||
|
||
export async function getTripoProviderConfigs(): Promise<TripoProviderConfig[]> {
|
||
const providers = await getAiProvidersByType({
|
||
type: "tripo-3d",
|
||
enabledOnly: true,
|
||
});
|
||
|
||
const normalizedProviders = providers
|
||
.map((provider): TripoProviderConfig | null => {
|
||
const providerApiKey = sanitizeTripoApiKey(String(provider.apiKey || ""));
|
||
if (!providerApiKey) return null;
|
||
return {
|
||
apiKey: providerApiKey,
|
||
baseUrl:
|
||
sanitizeTripoBaseUrl(String(provider.baseUrl || "")) ||
|
||
DEFAULT_TRIPO_BASE_URL,
|
||
providerId: provider.id,
|
||
providerName: provider.name,
|
||
};
|
||
})
|
||
.filter((item): item is TripoProviderConfig => Boolean(item));
|
||
|
||
if (normalizedProviders.length > 0) {
|
||
return normalizedProviders;
|
||
}
|
||
|
||
const envApiKey = sanitizeTripoApiKey(String(process.env.TRIPO_API_KEY || ""));
|
||
if (!envApiKey) {
|
||
return [];
|
||
}
|
||
|
||
return [
|
||
{
|
||
apiKey: envApiKey,
|
||
baseUrl:
|
||
sanitizeTripoBaseUrl(String(process.env.TRIPO_BASE_URL || "")) ||
|
||
DEFAULT_TRIPO_BASE_URL,
|
||
providerName: "TRIPO_API_KEY",
|
||
},
|
||
];
|
||
}
|
||
|
||
export async function getTripoProviderConfig(
|
||
preferredProviderId?: string | null,
|
||
): Promise<TripoProviderConfig> {
|
||
const providers = await getTripoProviderConfigs();
|
||
const normalizedPreferredProviderId = String(preferredProviderId || "").trim();
|
||
|
||
if (normalizedPreferredProviderId) {
|
||
const matchedProvider = providers.find(
|
||
(provider) => provider.providerId === normalizedPreferredProviderId,
|
||
);
|
||
if (matchedProvider) return matchedProvider;
|
||
}
|
||
|
||
const tripoProvider =
|
||
providers.length > 0
|
||
? providers[tripoProviderCursor % providers.length]
|
||
: null;
|
||
if (providers.length > 0) {
|
||
tripoProviderCursor = (tripoProviderCursor + 1) % providers.length;
|
||
}
|
||
if (tripoProvider) return tripoProvider;
|
||
|
||
throw new Error("未配置 Tripo3D API,请在系统设置-本地AI新增 Tripo3D 提供商");
|
||
}
|
||
|
||
export async function resolveTripoProviderConfigForTaskId(
|
||
taskId: string,
|
||
preferredProviderId?: string | null,
|
||
): Promise<TripoProviderConfig> {
|
||
const normalizedTaskId = String(taskId || "").trim();
|
||
if (!normalizedTaskId) {
|
||
return getTripoProviderConfig(preferredProviderId);
|
||
}
|
||
|
||
const providers = await getTripoProviderConfigs();
|
||
if (providers.length <= 1) {
|
||
return getTripoProviderConfig(preferredProviderId);
|
||
}
|
||
|
||
const normalizedPreferredProviderId = String(preferredProviderId || "").trim();
|
||
const preferredProvider = normalizedPreferredProviderId
|
||
? providers.find((provider) => provider.providerId === normalizedPreferredProviderId)
|
||
: null;
|
||
const orderedProviders = preferredProvider
|
||
? [
|
||
preferredProvider,
|
||
...providers.filter(
|
||
(provider) => provider.providerId !== preferredProvider.providerId,
|
||
),
|
||
]
|
||
: providers;
|
||
|
||
let lastTaskNotFoundError: TripoApiError | null = null;
|
||
let lastUnexpectedError: unknown = null;
|
||
|
||
for (const provider of orderedProviders) {
|
||
try {
|
||
await tripoRequest<Record<string, unknown>>(provider, {
|
||
method: "GET",
|
||
path: `/task/${encodeURIComponent(normalizedTaskId)}`,
|
||
});
|
||
return provider;
|
||
} catch (error) {
|
||
if (isTripoTaskNotFoundError(error)) {
|
||
lastTaskNotFoundError = error;
|
||
continue;
|
||
}
|
||
lastUnexpectedError = error;
|
||
}
|
||
}
|
||
|
||
if (lastUnexpectedError) {
|
||
throw lastUnexpectedError;
|
||
}
|
||
if (lastTaskNotFoundError) {
|
||
throw lastTaskNotFoundError;
|
||
}
|
||
return getTripoProviderConfig(preferredProviderId);
|
||
}
|