import { NextRequest, NextResponse } from "next/server"; import { and, desc, eq } from "drizzle-orm"; import { auth } from "@/app/(auth)/auth"; import { db } from "@/lib/db"; import { canvasDetailArchive, canvasProject } from "@/lib/db/schema"; type DetailArchiveBrief = { productSpu?: string; demandType?: string; productName?: string; targetAudience?: string; mainUser?: string; usageScenario?: string; triggerMoment?: string; coreNeed?: string; riskConcern?: string; expectedResult?: string; coreSellingPoint?: string; positioningStatement?: string; userVoice?: string; competitorReference?: string; priceBand?: string; brandTone?: string; bannedWords?: string; }; type DetailArchiveSlot = { url: string; name?: string; contentType?: string; } | null; type DetailArchiveSurfaceReview = { screenIndex?: number; action?: string; passed?: boolean; issues?: string[]; } | null; type DetailGenerationMode = "classic" | "agent_layout"; function sanitizeGenerationMode(value: unknown): DetailGenerationMode { return value === "agent_layout" ? "agent_layout" : "classic"; } function sanitizeBrief(input: unknown): DetailArchiveBrief { const raw = (input || {}) as Record; const normalize = (key: string) => String(raw[key] || "").replace(/\s+/g, " ").trim(); return { productSpu: normalize("productSpu"), demandType: normalize("demandType"), productName: normalize("productName"), targetAudience: normalize("targetAudience"), mainUser: normalize("mainUser"), usageScenario: normalize("usageScenario"), triggerMoment: normalize("triggerMoment"), coreNeed: normalize("coreNeed"), riskConcern: normalize("riskConcern"), expectedResult: normalize("expectedResult"), coreSellingPoint: normalize("coreSellingPoint"), positioningStatement: normalize("positioningStatement"), userVoice: normalize("userVoice"), competitorReference: normalize("competitorReference"), priceBand: normalize("priceBand"), brandTone: normalize("brandTone"), bannedWords: normalize("bannedWords"), }; } function sanitizeSlots(input: unknown): DetailArchiveSlot[] { if (!Array.isArray(input)) return []; return input.slice(0, 16).map((item) => { if (!item || typeof item !== "object") return null; const raw = item as Record; const url = String(raw.url || "").trim(); if (!url) return null; return { url, name: String(raw.name || "").trim() || undefined, contentType: String(raw.contentType || "").trim() || "image/jpeg", }; }); } function sanitizeUiSurfaceSnapshot(input: unknown) { if (!input || typeof input !== "object" || Array.isArray(input)) return null; return input as Record; } function sanitizeUiSurfaceLatestReview(input: unknown): DetailArchiveSurfaceReview { if (!input || typeof input !== "object" || Array.isArray(input)) return null; const raw = input as Record; const issues = Array.isArray(raw.issues) ? raw.issues.map((item) => String(item || "").trim()).filter(Boolean).slice(0, 10) : []; return { screenIndex: Number.isFinite(Number(raw.screenIndex)) ? Math.max(0, Number(raw.screenIndex)) : undefined, action: String(raw.action || "").trim() || undefined, passed: typeof raw.passed === "boolean" ? raw.passed : undefined, issues, }; } async function ensureProjectOwnership(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 Boolean(project); } export async function GET( _request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); } const { id: projectId } = await params; const ownsProject = await ensureProjectOwnership(projectId, session.user.id); if (!ownsProject) { return NextResponse.json({ success: false, error: "画布不存在或无权限" }, { status: 404 }); } const archives = await db .select() .from(canvasDetailArchive) .where( and( eq(canvasDetailArchive.projectId, projectId), eq(canvasDetailArchive.userId, session.user.id) ) ) .orderBy(desc(canvasDetailArchive.updatedAt)) .limit(30); return NextResponse.json({ success: true, data: archives }); } catch (error) { console.error("[CanvasDetailArchive] GET failed:", error); return NextResponse.json({ success: false, error: "获取详情存档失败" }, { status: 500 }); } } export async function POST( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); } const { id: projectId } = await params; const ownsProject = await ensureProjectOwnership(projectId, session.user.id); if (!ownsProject) { return NextResponse.json({ success: false, error: "画布不存在或无权限" }, { status: 404 }); } const body = await request.json(); const archiveRaw = (body?.archive || {}) as Record; const title = String(archiveRaw.title || "").trim() || "AI详情项目"; const prompt = String(archiveRaw.prompt || "").replace(/\s+/g, " ").trim(); const summary = String(archiveRaw.summary || "").trim(); const imageCount = Number(archiveRaw.imageCount); const coverUrl = String(archiveRaw.coverUrl || "").trim(); const generationMode = sanitizeGenerationMode(archiveRaw.generationMode); const brief = sanitizeBrief(archiveRaw.brief); const slots = sanitizeSlots(archiveRaw.slots); const uiSurfaceSnapshot = sanitizeUiSurfaceSnapshot(archiveRaw.uiSurfaceSnapshot); const uiSurfaceLatestReview = sanitizeUiSurfaceLatestReview(archiveRaw.uiSurfaceLatestReview); const [saved] = await db .insert(canvasDetailArchive) .values({ projectId, userId: session.user.id, title, prompt, generationMode, brief, slots, summary: summary || null, imageCount: Number.isFinite(imageCount) ? Math.max(0, Math.round(imageCount)) : null, coverUrl: coverUrl || null, uiSurfaceSnapshot, uiSurfaceLatestReview, }) .returning(); return NextResponse.json({ success: true, data: saved }); } catch (error) { console.error("[CanvasDetailArchive] POST failed:", error); return NextResponse.json({ success: false, error: "保存详情存档失败" }, { status: 500 }); } } export async function PATCH( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); } const { id: projectId } = await params; const ownsProject = await ensureProjectOwnership(projectId, session.user.id); if (!ownsProject) { return NextResponse.json({ success: false, error: "画布不存在或无权限" }, { status: 404 }); } const body = await request.json().catch(() => ({})); const archiveId = String((body as any)?.archiveId || "").trim(); const archiveRaw = ((body as any)?.archive || {}) as Record; if (!archiveId) { return NextResponse.json({ success: false, error: "缺少存档ID" }, { status: 400 }); } const title = String(archiveRaw.title || "").trim() || "AI详情项目"; const prompt = String(archiveRaw.prompt || "").replace(/\s+/g, " ").trim(); const summary = String(archiveRaw.summary || "").trim(); const imageCount = Number(archiveRaw.imageCount); const coverUrl = String(archiveRaw.coverUrl || "").trim(); const generationMode = sanitizeGenerationMode(archiveRaw.generationMode); const brief = sanitizeBrief(archiveRaw.brief); const slots = sanitizeSlots(archiveRaw.slots); const uiSurfaceSnapshot = sanitizeUiSurfaceSnapshot(archiveRaw.uiSurfaceSnapshot); const uiSurfaceLatestReview = sanitizeUiSurfaceLatestReview(archiveRaw.uiSurfaceLatestReview); const [updated] = await db .update(canvasDetailArchive) .set({ title, prompt, generationMode, brief, slots, summary: summary || null, imageCount: Number.isFinite(imageCount) ? Math.max(0, Math.round(imageCount)) : null, coverUrl: coverUrl || null, uiSurfaceSnapshot, uiSurfaceLatestReview, updatedAt: new Date(), }) .where( and( eq(canvasDetailArchive.id, archiveId), eq(canvasDetailArchive.projectId, projectId), eq(canvasDetailArchive.userId, session.user.id) ) ) .returning(); if (!updated) { return NextResponse.json({ success: false, error: "存档不存在或无权限" }, { status: 404 }); } return NextResponse.json({ success: true, data: updated }); } catch (error) { console.error("[CanvasDetailArchive] PATCH failed:", error); return NextResponse.json({ success: false, error: "更新详情存档失败" }, { status: 500 }); } } export async function DELETE( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); } const { id: projectId } = await params; const ownsProject = await ensureProjectOwnership(projectId, session.user.id); if (!ownsProject) { return NextResponse.json({ success: false, error: "画布不存在或无权限" }, { status: 404 }); } const searchArchiveId = request.nextUrl.searchParams.get("archiveId"); let bodyArchiveId = ""; if (!searchArchiveId) { const body = await request.json().catch(() => ({})); bodyArchiveId = String((body as any)?.archiveId || "").trim(); } const archiveId = String(searchArchiveId || bodyArchiveId || "").trim(); if (!archiveId) { return NextResponse.json({ success: false, error: "缺少存档ID" }, { status: 400 }); } const [deleted] = await db .delete(canvasDetailArchive) .where( and( eq(canvasDetailArchive.id, archiveId), eq(canvasDetailArchive.projectId, projectId), eq(canvasDetailArchive.userId, session.user.id) ) ) .returning(); if (!deleted) { return NextResponse.json({ success: false, error: "存档不存在或无权限" }, { status: 404 }); } return NextResponse.json({ success: true, data: deleted }); } catch (error) { console.error("[CanvasDetailArchive] DELETE failed:", error); return NextResponse.json({ success: false, error: "删除详情存档失败" }, { status: 500 }); } }