import { and, eq } from "drizzle-orm"; import { NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { CANVAS_LAYOUT_REPLACE_BATCH_LIMIT, createCanvasLayoutReplaceBatch, getCanvasLayoutReplaceBatchActiveTaskId, getLatestCanvasLayoutReplaceBatchForUser, type CanvasLayoutReplaceBatchSource, } from "@/lib/canvas/layout-replace-batch"; import { enqueueCanvasImageTask, ensureCanvasImageTaskActive, } from "@/lib/canvas/image-task-queue"; import { db } from "@/lib/db"; import { canvasProject } from "@/lib/db/schema"; import { getPointsSystemConfig } from "@/lib/db/queries"; import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge"; import { getImageToolCreditCost } from "@/lib/points/config"; export const maxDuration = 1200; function normalizeHttpUrl(value: unknown) { const url = String(value || "").trim(); return /^https?:\/\//i.test(url) ? url : ""; } function normalizeUuid(value: unknown) { const id = String(value || "").trim(); return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id) ? id : ""; } function normalizeSources(value: unknown): CanvasLayoutReplaceBatchSource[] { if (!Array.isArray(value)) return []; const seen = new Set(); const sources: CanvasLayoutReplaceBatchSource[] = []; for (const item of value) { if (!item || typeof item !== "object" || Array.isArray(item)) continue; const record = item as Record; const sourceImageUrl = normalizeHttpUrl(record.sourceImageUrl); if (!sourceImageUrl || seen.has(sourceImageUrl)) continue; seen.add(sourceImageUrl); sources.push({ sourceImageUrl, sourceKind: record.sourceKind === "upload" ? "upload" : "canvas", sourceNodeId: String(record.sourceNodeId || "").trim() || undefined, sourceTitle: String(record.sourceTitle || "").trim().slice(0, 256) || undefined, }); if (sources.length >= CANVAS_LAYOUT_REPLACE_BATCH_LIMIT) break; } return sources; } async function getOwnedProject(projectId: string, userId: string) { const [project] = await db .select({ id: canvasProject.id }) .from(canvasProject) .where(and(eq(canvasProject.id, projectId), eq(canvasProject.userId, userId))) .limit(1); return project || null; } export async function GET(request: Request) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); } const { searchParams } = new URL(request.url); const projectId = normalizeUuid(searchParams.get("projectId")); const sourceNodeId = String(searchParams.get("sourceNodeId") || "").trim(); if (!projectId) { return NextResponse.json( { success: false, error: "缺少有效的画布项目" }, { status: 400 } ); } if (!(await getOwnedProject(projectId, session.user.id))) { return NextResponse.json({ success: false, error: "画布不存在" }, { status: 404 }); } let batch = await getLatestCanvasLayoutReplaceBatchForUser({ projectId, userId: session.user.id, sourceNodeId: sourceNodeId || undefined, }); const activeTaskId = batch ? getCanvasLayoutReplaceBatchActiveTaskId(batch) : null; if (activeTaskId) { await ensureCanvasImageTaskActive({ taskId: activeTaskId, userId: session.user.id, }); batch = await getLatestCanvasLayoutReplaceBatchForUser({ projectId, userId: session.user.id, sourceNodeId: sourceNodeId || undefined, }); } return NextResponse.json({ success: true, batch: batch || null }); } catch (error) { console.error("[Canvas Layout Replace Batch][GET] Error:", error); return NextResponse.json( { success: false, error: error instanceof Error ? error.message : "查询批次失败", }, { status: 500 } ); } } export async function POST(request: Request) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); } const body = await request.json().catch(() => ({})); const projectId = normalizeUuid(body?.projectId); const referenceImageUrl = normalizeHttpUrl(body?.referenceImageUrl); const sources = normalizeSources(body?.sources); if (!projectId || !(await getOwnedProject(projectId, session.user.id))) { return NextResponse.json({ success: false, error: "画布不存在" }, { status: 404 }); } if (sources.length === 0) { return NextResponse.json( { success: false, error: "请至少添加一张待处理图片" }, { status: 400 } ); } if (!referenceImageUrl) { return NextResponse.json( { success: false, error: "请上传目标模版图" }, { status: 400 } ); } const pointsConfig = await getPointsSystemConfig(); const perImageCost = getImageToolCreditCost(pointsConfig, "repaint"); const totalCost = perImageCost * sources.length; const pointsCharge = await prepareFeaturePointsCharge({ userId: session.user.id, unitCost: totalCost, reason: "image_repaint", modelId: "canvas-layout-replace-batch", note: `批量排版替换 ${sources.length} 张`, pointsConfig, }); if (!pointsCharge.ok) { return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status }); } const created = await createCanvasLayoutReplaceBatch({ userId: session.user.id, projectId, sources, referenceImageUrl, supplementalPrompt: typeof body?.supplementalPrompt === "string" ? body.supplementalPrompt : undefined, imageSize: typeof body?.imageSize === "string" ? body.imageSize : "2K", model: typeof body?.model === "string" ? body.model : undefined, }); await enqueueCanvasImageTask(created.firstTaskId); if (pointsCharge.chargeable) { try { await pointsCharge.consume({ note: `批量排版替换 ${sources.length} 张(${created.batchId})`, }); } catch (error) { console.error("[Points] Failed to consume layout batch points:", error); } } return NextResponse.json({ success: true, batchId: created.batchId, firstTaskId: created.firstTaskId, sourceCount: sources.length, pointsCost: totalCost, }); } catch (error) { console.error("[Canvas Layout Replace Batch][POST] Error:", error); return NextResponse.json( { success: false, error: error instanceof Error ? error.message : "创建批次失败", }, { status: 500 } ); } }