252 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { 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, thumbnail, canvasData } = body;
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 });
}
}