import { NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { TripoApiError, pickTaskModelUrl, pickTaskPreviewImageUrl, tripoRequest, } from "@/lib/tripo/client"; import { getTripoProviderConfig } from "@/lib/tripo/server"; import type { TripoTaskData } from "@/lib/tripo/types"; import { uploadFile } from "@/lib/storage/service"; export const runtime = "nodejs"; export const maxDuration = 30; type TripoModelArchiveCacheItem = { sourceUrl: string; archivedUrl: string; }; function shouldArchiveTripoModelUrl(modelUrl?: string | null) { const raw = String(modelUrl || "").trim(); if (!raw) return false; let parsed: URL; try { parsed = new URL(raw); } catch { return false; } const hostname = parsed.hostname.toLowerCase(); return ( hostname === "tripo3d.com" || hostname.endsWith(".tripo3d.com") ); } function resolveModelExtension(modelUrl: string) { try { const parsed = new URL(modelUrl); const pathname = parsed.pathname.toLowerCase(); const ext = pathname.split(".").pop() || ""; const allowed = new Set(["glb", "gltf", "usdz", "fbx", "obj", "stl", "3mf"]); if (allowed.has(ext)) return ext; } catch { // ignore } return "glb"; } function getTripoArchiveCache() { const globalState = globalThis as any; if (!globalState.__tripoModelArchiveCache) { globalState.__tripoModelArchiveCache = new Map(); } return globalState.__tripoModelArchiveCache as Map; } function getTripoArchiveInflight() { const globalState = globalThis as any; if (!globalState.__tripoModelArchiveInflight) { globalState.__tripoModelArchiveInflight = new Map>(); } return globalState.__tripoModelArchiveInflight as Map>; } async function archiveTripoModelIfNeeded(params: { taskId: string; modelUrl?: string | null; }) { const modelUrl = String(params.modelUrl || "").trim(); if (!shouldArchiveTripoModelUrl(modelUrl)) { return modelUrl || null; } const cache = getTripoArchiveCache(); const cached = cache.get(params.taskId); if (cached && cached.sourceUrl === modelUrl) { return cached.archivedUrl; } const inflight = getTripoArchiveInflight(); const existingTask = inflight.get(params.taskId); if (existingTask) { const result = await existingTask; return result || modelUrl; } const archiveTask = (async () => { try { const upstream = await fetch(modelUrl, { method: "GET", headers: { "User-Agent": "AI-Chatbot-3D-Archiver/1.0", }, cache: "no-store", }); if (!upstream.ok) { throw new Error(`下载 Tripo 模型失败(HTTP ${upstream.status})`); } const modelBuffer = await upstream.arrayBuffer(); if (!modelBuffer || modelBuffer.byteLength <= 0) { throw new Error("Tripo 模型文件为空"); } const ext = resolveModelExtension(modelUrl); const filename = `tripo-model-${params.taskId}.${ext}`; const uploaded = await uploadFile(filename, modelBuffer); cache.set(params.taskId, { sourceUrl: modelUrl, archivedUrl: uploaded.url, }); console.log("[TripoModelArchive] success", { taskId: params.taskId, sourceHost: new URL(modelUrl).hostname, bytes: modelBuffer.byteLength, archivedUrl: uploaded.url, }); return uploaded.url; } catch (error) { console.error("[TripoModelArchive] failed", { taskId: params.taskId, sourceUrl: modelUrl, error: error instanceof Error ? error.message : String(error), }); return null; } finally { inflight.delete(params.taskId); } })(); inflight.set(params.taskId, archiveTask); const archivedUrl = await archiveTask; return archivedUrl || modelUrl; } interface RouteParams { params: Promise<{ taskId: string }>; } export async function GET(_request: Request, { params }: RouteParams) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); } const { taskId } = await params; const normalizedTaskId = String(taskId || "").trim(); if (!normalizedTaskId) { return NextResponse.json({ success: false, error: "缺少 taskId" }, { status: 400 }); } const config = await getTripoProviderConfig(); const result = await tripoRequest(config, { method: "GET", path: `/task/${encodeURIComponent(normalizedTaskId)}`, }); const output = result.data?.output && typeof result.data.output === "object" ? result.data.output : {}; const modelUrl = pickTaskModelUrl(output as Record); const previewImageUrl = pickTaskPreviewImageUrl(output as Record); const stableModelUrl = await archiveTripoModelIfNeeded({ taskId: normalizedTaskId, modelUrl, }); return NextResponse.json({ success: true, data: { taskId: result.data.task_id, type: result.data.type, status: result.data.status, progress: result.data.progress ?? null, output, modelUrl: stableModelUrl, previewImageUrl, errorCode: result.data.error_code ?? null, errorMessage: result.data.error_msg ?? null, }, traceId: result.traceId, }); } catch (error) { if (error instanceof TripoApiError) { return NextResponse.json( { success: false, error: error.message, code: error.code, suggestion: error.suggestion, traceId: error.traceId, }, { status: error.httpStatus || 502 } ); } return NextResponse.json( { success: false, error: error instanceof Error ? error.message : "查询任务失败", }, { status: 500 } ); } }