fix(model3d): preserve Tripo provider integration

This commit is contained in:
134 2026-08-09 15:26:10 +08:00
parent 2489ce54f2
commit 1b281a76dc
4 changed files with 7 additions and 207 deletions

View File

@ -17,7 +17,6 @@ import {
resolveTripoProviderConfigForTaskId,
} from "@/lib/tripo/server";
import type { TripoTaskData } from "@/lib/tripo/types";
import { getConfiguredVip98kTaskStatus } from "@/lib/ai/vip98k-runtime";
export const runtime = "nodejs";
export const maxDuration = 120;
@ -320,111 +319,6 @@ export async function GET(request: Request, { params }: RouteParams) {
const { searchParams } = new URL(request.url);
const preferredProviderId = searchParams.get("providerId");
if (normalizedTaskId.startsWith("vip98k:")) {
const [, encodedProviderId, ...remoteTaskParts] = normalizedTaskId.split(":");
const remoteTaskId = remoteTaskParts.join(":");
if (!encodedProviderId || !remoteTaskId) {
return NextResponse.json(
{ success: false, error: "VIP98K 3D taskId 格式无效" },
{ status: 400 },
);
}
const result = await getConfiguredVip98kTaskStatus({
taskId: remoteTaskId,
preferredProviderId: encodedProviderId,
});
const modelUrl =
result.outputUrls.find((url) =>
/\.(?:glb|gltf|fbx|obj|stl|3mf|usdz|ply|zip)(?:[?#].*)?$/i.test(url),
) || result.outputUrls[0] || null;
const previewImageUrl =
result.outputUrls.find((url) =>
/\.(?:png|jpe?g|webp|gif)(?:[?#].*)?$/i.test(url),
) || null;
const stableModelUrl = await archiveTripoModelIfNeeded({
taskId: remoteTaskId,
cacheKey: `vip98k:${remoteTaskId}:model`,
sourceUrl: modelUrl,
filenamePrefix: `vip98k-model-${remoteTaskId}`,
fallbackExtension: "glb",
});
const stablePreviewImageUrl = await archiveTripoModelIfNeeded({
taskId: remoteTaskId,
cacheKey: `vip98k:${remoteTaskId}:preview`,
sourceUrl: previewImageUrl,
filenamePrefix: `vip98k-preview-${remoteTaskId}`,
fallbackExtension: "jpg",
});
const errorMessage = result.failed ? "VIP98K 3D 任务执行失败" : null;
const refundedPoints = result.failed
? await refundModel3DPointsForFailedTask({
userId: session.user.id,
taskId: normalizedTaskId,
errorMessage: errorMessage || "VIP98K 3D task failed",
})
: 0;
return NextResponse.json({
success: true,
data: {
taskId: normalizedTaskId,
type: "model",
status: result.failed ? "failed" : result.done ? "success" : result.status || "running",
progress: result.done ? 100 : null,
output: result.payload,
modelUrl: stableModelUrl,
previewImageUrl: stablePreviewImageUrl,
assetPayload: {
providerId: result.providerId,
providerName: result.providerName,
storageArchivedAt: stableModelUrl || stablePreviewImageUrl ? new Date().toISOString() : null,
},
errorCode: result.failed ? "VIP98K_TASK_FAILED" : null,
errorMessage,
refundedPoints,
},
});
}
if (normalizedTaskId.startsWith("vip98k-direct:")) {
const [, encodedProviderId, encodedUrls] = normalizedTaskId.split(":");
if (!encodedProviderId || !encodedUrls) {
return NextResponse.json(
{ success: false, error: "VIP98K 3D direct taskId 格式无效" },
{ status: 400 },
);
}
const outputUrls = JSON.parse(
Buffer.from(encodedUrls, "base64url").toString("utf8"),
) as string[];
const modelUrl =
outputUrls.find((url) =>
/\.(?:glb|gltf|fbx|obj|stl|3mf|usdz|ply|zip)(?:[?#].*)?$/i.test(url),
) || outputUrls[0] || null;
const previewImageUrl =
outputUrls.find((url) => /\.(?:png|jpe?g|webp|gif)(?:[?#].*)?$/i.test(url)) ||
null;
return NextResponse.json({
success: true,
data: {
taskId: normalizedTaskId,
type: "model",
status: "success",
progress: 100,
output: { urls: outputUrls },
modelUrl,
previewImageUrl,
assetPayload: {
providerId: encodedProviderId,
providerName: "VIP98K",
storageArchivedAt: null,
},
errorCode: null,
errorMessage: null,
refundedPoints: 0,
},
});
}
const config = preferredProviderId
? await getTripoProviderConfig(preferredProviderId)
: await resolveTripoProviderConfigForTaskId(normalizedTaskId);

View File

@ -14,10 +14,6 @@ import {
resolveTripoProviderConfigForTaskId,
} from "@/lib/tripo/server";
import { TripoApiError, tripoRequest } from "@/lib/tripo/client";
import {
getVip98kRuntimeProvider,
submitConfiguredVip98kTask,
} from "@/lib/ai/vip98k-runtime";
export const runtime = "nodejs";
export const maxDuration = 60;
@ -120,13 +116,6 @@ function assignMapped(
}
}
const VIP98K_TEXT_TO_3D_MODEL = "tencent/hunyuan3d-pro/text-to-3d";
const VIP98K_IMAGE_TO_3D_MODEL = "tencent/hunyuan3d-pro/image-to-3d";
function encodeVip98kDirectResult(outputUrls: string[]) {
return Buffer.from(JSON.stringify(outputUrls), "utf8").toString("base64url");
}
export async function POST(request: Request) {
try {
const session = await auth();
@ -426,73 +415,6 @@ export async function POST(request: Request) {
}
const preferredProviderId = readOptionalTrimmedText(body.providerId);
let vip98kProvider: Awaited<ReturnType<typeof getVip98kRuntimeProvider>> | null = null;
try {
vip98kProvider = await getVip98kRuntimeProvider(preferredProviderId);
} catch {
vip98kProvider = null;
}
if (vip98kProvider) {
if (taskType !== "text_to_model" && taskType !== "image_to_model") {
return NextResponse.json(
{
success: false,
error: "VIP98K 当前仅支持文生 3D 和图生 3D;细化、动画和格式转换暂不支持",
},
{ status: 400 },
);
}
const inputFile = body.file && typeof body.file === "object" ? body.file : null;
const imageUrl = String(body.imageUrl || inputFile?.url || "").trim();
if (taskType === "image_to_model" && !imageUrl) {
return NextResponse.json(
{ success: false, error: "VIP98K 图生 3D 缺少可访问的图片地址" },
{ status: 400 },
);
}
const vipModelId =
taskType === "image_to_model"
? VIP98K_IMAGE_TO_3D_MODEL
: VIP98K_TEXT_TO_3D_MODEL;
const submission = await submitConfiguredVip98kTask({
preferredProviderId: vip98kProvider.id,
modelId: vipModelId,
kind: "model-3d",
prompt: String(body.prompt || "Generate a production-ready 3D model"),
imageUrls: imageUrl ? [imageUrl] : [],
});
if (!submission.taskId && submission.outputUrls.length === 0) {
throw new Error("VIP98K 3D 任务未返回 taskId 或模型地址");
}
const taskId = submission.taskId
? `vip98k:${vip98kProvider.id}:${submission.taskId}`
: `vip98k-direct:${vip98kProvider.id}:${encodeVip98kDirectResult(submission.outputUrls)}`;
if (pointsCharge.chargeable) {
await pointsCharge.consume({
note: `3D模型任务消耗积分(${billableTaskLabel} / ${taskId})`,
}).catch((error) => {
logger.error("VIP98K points consume failed", error, {
taskId,
userId: session.user.id,
});
});
}
return NextResponse.json({
success: true,
data: {
taskId,
status: submission.outputUrls.length > 0 ? "success" : "queued",
providerId: vip98kProvider.id,
providerName: vip98kProvider.name,
raw: submission.payload,
},
});
}
const dependencyTaskId =
taskType === "refine_model"
? String(body.draftModelTaskId || "").trim()

View File

@ -2,8 +2,6 @@ import { NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
import { getTripoProviderConfig } from "@/lib/tripo/server";
import { TripoApiError, tripoRequest } from "@/lib/tripo/client";
import { getVip98kRuntimeProvider } from "@/lib/ai/vip98k-runtime";
import { uploadFile } from "@/lib/storage/service";
export const runtime = "nodejs";
export const maxDuration = 60;
@ -53,26 +51,7 @@ export async function POST(request: Request) {
);
}
let config: Awaited<ReturnType<typeof getTripoProviderConfig>> | null = null;
try {
config = await getTripoProviderConfig(providerId);
} catch {
const vip98k = await getVip98kRuntimeProvider(providerId);
const extension = file.type === "image/png" ? "png" : file.type === "image/webp" ? "webp" : "jpg";
const uploaded = await uploadFile(
`vip98k-3d-input-${Date.now()}-${crypto.randomUUID()}.${extension}`,
await file.arrayBuffer(),
);
return NextResponse.json({
success: true,
data: {
imageToken: null,
file: { type: extension, url: uploaded.url },
providerId: vip98k.id,
providerName: vip98k.name,
},
});
}
const config = await getTripoProviderConfig(providerId);
const uploadPaths = ["/upload/sts", "/upload"];
let lastError: unknown = null;

View File

@ -12,7 +12,12 @@ import {
dotenv.config({ path: ".env.local" });
dotenv.config();
const KEEP_PROVIDER_TYPES = new Set(["vip98k", "fal-image", "fal-video"]);
const KEEP_PROVIDER_TYPES = new Set([
"vip98k",
"fal-image",
"fal-video",
"tripo-3d",
]);
const REQUIRED_CAPABILITY_MODELS = [
"nano-banana-2",
"remove-background",