254 lines
7.7 KiB
TypeScript
254 lines
7.7 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
||
import { auth } from '@/app/(auth)/auth';
|
||
import { db } from '@/lib/db';
|
||
import { canvasProject } from '@/lib/db/schema';
|
||
import { syncCanvasNodesForProject } from '@/lib/db/queries';
|
||
import { resolveArchivedImageUrls } from '@/lib/storage/image-archive-db';
|
||
import { eq, and } from 'drizzle-orm';
|
||
import {
|
||
buildStaleCanvasSaveConflictPayload,
|
||
isStaleCanvasSaveBaseRevision,
|
||
} from '@/lib/canvas/canvas-save-conflict';
|
||
|
||
function resolveWorkspaceType(input: unknown) {
|
||
return input === "agent" || input === "video" ? input : "canvas";
|
||
}
|
||
|
||
function getDefaultWorkspaceTitle(workspaceType: ReturnType<typeof resolveWorkspaceType>) {
|
||
return workspaceType === "video" ? "未命名视频画布" : "未命名画布";
|
||
}
|
||
|
||
function shouldUseLocalCanvasE2E(request: NextRequest) {
|
||
const hostname = request.nextUrl.hostname;
|
||
return (
|
||
process.env.CANVAS_E2E_LOCAL === "true" &&
|
||
(hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1")
|
||
);
|
||
}
|
||
|
||
function createLocalCanvasProject(
|
||
id: string,
|
||
input: {
|
||
canvasData?: unknown;
|
||
description?: unknown;
|
||
thumbnail?: unknown;
|
||
title?: unknown;
|
||
workspaceType?: unknown;
|
||
} = {}
|
||
) {
|
||
const now = new Date().toISOString();
|
||
const workspaceType = resolveWorkspaceType(input.workspaceType || "video");
|
||
return {
|
||
id,
|
||
userId: "local-video-canvas-e2e-user",
|
||
title: String(input.title || getDefaultWorkspaceTitle(workspaceType)),
|
||
description: typeof input.description === "string" ? input.description : null,
|
||
thumbnail: typeof input.thumbnail === "string" ? input.thumbnail : null,
|
||
workspaceType,
|
||
canvasData:
|
||
input.canvasData && typeof input.canvasData === "object"
|
||
? input.canvasData
|
||
: {
|
||
nodes: [],
|
||
edges: [],
|
||
selectedNodeIds: [],
|
||
viewport: { x: 0, y: 0, zoom: 1 },
|
||
tool: "select",
|
||
},
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
};
|
||
}
|
||
|
||
// 获取单个画布
|
||
export async function GET(
|
||
request: NextRequest,
|
||
{ params }: { params: Promise<{ id: string }> }
|
||
) {
|
||
try {
|
||
const { id } = await params;
|
||
if (shouldUseLocalCanvasE2E(request)) {
|
||
return NextResponse.json({
|
||
success: true,
|
||
data: createLocalCanvasProject(id),
|
||
});
|
||
}
|
||
|
||
const session = await auth();
|
||
if (!session?.user?.id) {
|
||
return NextResponse.json({ success: false, error: '未登录' }, { status: 401 });
|
||
}
|
||
|
||
const [project] = await db
|
||
.select()
|
||
.from(canvasProject)
|
||
.where(
|
||
and(
|
||
eq(canvasProject.id, id),
|
||
eq(canvasProject.userId, session.user.id)
|
||
)
|
||
);
|
||
|
||
if (!project) {
|
||
return NextResponse.json({ success: false, error: '画布不存在' }, { status: 404 });
|
||
}
|
||
|
||
return NextResponse.json({ success: true, data: project });
|
||
} catch (error) {
|
||
console.error('获取画布失败:', error);
|
||
return NextResponse.json({ success: false, error: '获取画布失败' }, { status: 500 });
|
||
}
|
||
}
|
||
|
||
// 更新画布(支持 upsert:如果不存在则创建)
|
||
export async function PUT(
|
||
request: NextRequest,
|
||
{ params }: { params: Promise<{ id: string }> },
|
||
) {
|
||
try {
|
||
const { id } = await params;
|
||
const body = await request.json();
|
||
if (shouldUseLocalCanvasE2E(request)) {
|
||
return NextResponse.json({
|
||
success: true,
|
||
data: createLocalCanvasProject(id, body),
|
||
});
|
||
}
|
||
|
||
const session = await auth();
|
||
if (!session?.user?.id) {
|
||
return NextResponse.json(
|
||
{ success: false, error: "未登录" },
|
||
{ status: 401 },
|
||
);
|
||
}
|
||
|
||
const { title, description, canvasData } = body;
|
||
const thumbnail = await resolveArchivedImageUrls(body.thumbnail);
|
||
const baseCanvasRevision =
|
||
body?.baseCanvasUpdatedAt || body?.baseCanvasRevision;
|
||
const workspaceType =
|
||
body && Object.prototype.hasOwnProperty.call(body, "workspaceType")
|
||
? resolveWorkspaceType(body.workspaceType)
|
||
: undefined;
|
||
|
||
return await db.transaction(async (tx) => {
|
||
const [existingProject] = await tx
|
||
.select()
|
||
.from(canvasProject)
|
||
.where(
|
||
and(
|
||
eq(canvasProject.id, id),
|
||
eq(canvasProject.userId, session.user.id),
|
||
),
|
||
)
|
||
.for("update");
|
||
if (
|
||
existingProject &&
|
||
canvasData !== undefined &&
|
||
baseCanvasRevision &&
|
||
isStaleCanvasSaveBaseRevision({
|
||
baseCanvasRevision: body?.baseCanvasRevision,
|
||
baseCanvasUpdatedAt: body?.baseCanvasUpdatedAt,
|
||
currentUpdatedAt: existingProject.updatedAt,
|
||
})
|
||
) {
|
||
return NextResponse.json(
|
||
buildStaleCanvasSaveConflictPayload(existingProject.updatedAt),
|
||
{ status: 409 },
|
||
);
|
||
}
|
||
if (!existingProject) {
|
||
// The parent must exist before node/asset foreign keys are synchronized.
|
||
const [created] = await tx
|
||
.insert(canvasProject)
|
||
.values({
|
||
id,
|
||
userId: session.user.id,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
title: title || getDefaultWorkspaceTitle(workspaceType || "canvas"),
|
||
workspaceType: workspaceType || "canvas",
|
||
canvasData: {
|
||
nodes: [],
|
||
edges: [],
|
||
viewport: { x: 0, y: 0, zoom: 1 },
|
||
},
|
||
})
|
||
.onConflictDoNothing({ target: canvasProject.id })
|
||
.returning();
|
||
if (!created)
|
||
return NextResponse.json(
|
||
{ success: false, error: "画布已被创建,请重新加载后保存" },
|
||
{ status: 409 },
|
||
);
|
||
}
|
||
const syncedCanvasData =
|
||
canvasData !== undefined
|
||
? await syncCanvasNodesForProject(
|
||
{ projectId: id, userId: session.user.id, canvasData },
|
||
tx,
|
||
)
|
||
: undefined;
|
||
const updateData: any = { updatedAt: new Date(Math.max(Date.now(), (existingProject?.updatedAt.getTime() || 0) + 1)) };
|
||
if (title !== undefined) updateData.title = title;
|
||
if (description !== undefined) updateData.description = description;
|
||
if (thumbnail !== undefined) updateData.thumbnail = thumbnail;
|
||
if (workspaceType !== undefined) updateData.workspaceType = workspaceType;
|
||
if (syncedCanvasData !== undefined)
|
||
updateData.canvasData = syncedCanvasData;
|
||
const [saved] = await tx
|
||
.update(canvasProject)
|
||
.set(updateData)
|
||
.where(
|
||
and(
|
||
eq(canvasProject.id, id),
|
||
eq(canvasProject.userId, session.user.id),
|
||
),
|
||
)
|
||
.returning();
|
||
return NextResponse.json({ success: true, data: saved });
|
||
});
|
||
} catch (error) {
|
||
console.error("更新/创建画布失败:", 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 } = await params;
|
||
|
||
const [deletedProject] = await db
|
||
.delete(canvasProject)
|
||
.where(
|
||
and(
|
||
eq(canvasProject.id, id),
|
||
eq(canvasProject.userId, session.user.id)
|
||
)
|
||
)
|
||
.returning();
|
||
|
||
if (!deletedProject) {
|
||
return NextResponse.json({ success: false, error: '画布不存在' }, { status: 404 });
|
||
}
|
||
|
||
return NextResponse.json({ success: true, data: deletedProject });
|
||
} catch (error) {
|
||
console.error('删除画布失败:', error);
|
||
return NextResponse.json({ success: false, error: '删除画布失败' }, { status: 500 });
|
||
}
|
||
}
|