321 lines
11 KiB
TypeScript
321 lines
11 KiB
TypeScript
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
}
|
|
|
|
function sanitizeUiSurfaceLatestReview(input: unknown): DetailArchiveSurfaceReview {
|
|
if (!input || typeof input !== "object" || Array.isArray(input)) return null;
|
|
const raw = input as Record<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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 });
|
|
}
|
|
}
|