275 lines
8.5 KiB
TypeScript
275 lines
8.5 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 { eq, and, inArray } from 'drizzle-orm';
|
||
import {
|
||
buildStaleCanvasSaveConflictPayload,
|
||
isStaleCanvasSaveBaseRevision,
|
||
} from '@/lib/canvas/canvas-save-conflict';
|
||
import { normalizeCanvasProjectTitle } from '@/lib/canvas/project-title';
|
||
import {
|
||
getExplicitVideoCanvasWorkspaceMeta,
|
||
getVideoCanvasWorkspaceProjects,
|
||
withVideoCanvasWorkspaceMeta,
|
||
} from '@/lib/canvas/video-workspace';
|
||
|
||
function resolveWorkspaceType(input: unknown) {
|
||
return input === "agent" || input === "video" ? input : "canvas";
|
||
}
|
||
|
||
function getDefaultWorkspaceTitle(workspaceType: ReturnType<typeof resolveWorkspaceType>) {
|
||
return workspaceType === "video" ? "未命名视频画布" : "未命名画布";
|
||
}
|
||
|
||
// 获取单个画布
|
||
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 } = await params;
|
||
|
||
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 session = await auth();
|
||
if (!session?.user?.id) {
|
||
return NextResponse.json({ success: false, error: '未登录' }, { status: 401 });
|
||
}
|
||
|
||
const { id } = await params;
|
||
const body = await request.json();
|
||
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;
|
||
|
||
// 先检查画布是否存在
|
||
const [existingProject] = await db
|
||
.select()
|
||
.from(canvasProject)
|
||
.where(
|
||
and(
|
||
eq(canvasProject.id, id),
|
||
eq(canvasProject.userId, session.user.id)
|
||
)
|
||
);
|
||
|
||
if (existingProject) {
|
||
if (
|
||
canvasData !== undefined &&
|
||
baseCanvasRevision &&
|
||
isStaleCanvasSaveBaseRevision({
|
||
baseCanvasRevision: body?.baseCanvasRevision,
|
||
baseCanvasUpdatedAt: body?.baseCanvasUpdatedAt,
|
||
currentUpdatedAt: existingProject.updatedAt,
|
||
})
|
||
) {
|
||
return NextResponse.json(
|
||
buildStaleCanvasSaveConflictPayload(existingProject.updatedAt),
|
||
{ status: 409 }
|
||
);
|
||
}
|
||
|
||
const explicitWorkspaceMeta = getExplicitVideoCanvasWorkspaceMeta(
|
||
existingProject.canvasData
|
||
);
|
||
const canvasDataWithWorkspace =
|
||
canvasData !== undefined && explicitWorkspaceMeta
|
||
? withVideoCanvasWorkspaceMeta(canvasData, explicitWorkspaceMeta)
|
||
: canvasData;
|
||
const syncedCanvasData =
|
||
canvasDataWithWorkspace !== undefined
|
||
? await syncCanvasNodesForProject({
|
||
projectId: id,
|
||
userId: session.user.id,
|
||
canvasData: canvasDataWithWorkspace,
|
||
})
|
||
: undefined;
|
||
|
||
// 更新现有画布
|
||
const updateData: any = { updatedAt: new Date() };
|
||
if (title !== undefined) {
|
||
updateData.title = normalizeCanvasProjectTitle(
|
||
title,
|
||
existingProject.title
|
||
);
|
||
if (existingProject.workspaceType === "video") {
|
||
const videoProjects = await db
|
||
.select({
|
||
canvasData: canvasProject.canvasData,
|
||
id: canvasProject.id,
|
||
})
|
||
.from(canvasProject)
|
||
.where(
|
||
and(
|
||
eq(canvasProject.userId, session.user.id),
|
||
eq(canvasProject.workspaceType, "video")
|
||
)
|
||
);
|
||
const workspaceProjectIds = getVideoCanvasWorkspaceProjects(
|
||
videoProjects,
|
||
existingProject
|
||
)
|
||
.map((project) => project.id)
|
||
.filter((projectId) => projectId !== id);
|
||
if (workspaceProjectIds.length > 0) {
|
||
await db
|
||
.update(canvasProject)
|
||
.set({ title: updateData.title, updatedAt: new Date() })
|
||
.where(inArray(canvasProject.id, workspaceProjectIds));
|
||
}
|
||
}
|
||
}
|
||
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 [updatedProject] = await db
|
||
.update(canvasProject)
|
||
.set(updateData)
|
||
.where(
|
||
and(
|
||
eq(canvasProject.id, id),
|
||
eq(canvasProject.userId, session.user.id)
|
||
)
|
||
)
|
||
.returning();
|
||
|
||
return NextResponse.json({ success: true, data: updatedProject });
|
||
} else {
|
||
const syncedCanvasData =
|
||
canvasData !== undefined
|
||
? await syncCanvasNodesForProject({
|
||
projectId: id,
|
||
userId: session.user.id,
|
||
canvasData,
|
||
})
|
||
: undefined;
|
||
|
||
const nextWorkspaceType = workspaceType || "canvas";
|
||
const nextCanvasData = syncedCanvasData || {
|
||
nodes: [],
|
||
edges: [],
|
||
viewport: { x: 0, y: 0, zoom: 1 },
|
||
};
|
||
const nextTitle = normalizeCanvasProjectTitle(
|
||
title,
|
||
getDefaultWorkspaceTitle(nextWorkspaceType)
|
||
);
|
||
const insertData: any = {
|
||
id, // 使用指定的 ID
|
||
userId: session.user.id,
|
||
title: nextTitle,
|
||
workspaceType: nextWorkspaceType,
|
||
canvasData: nextCanvasData,
|
||
};
|
||
const conflictUpdateData: any = {
|
||
title: nextTitle,
|
||
workspaceType: nextWorkspaceType,
|
||
canvasData: nextCanvasData,
|
||
updatedAt: new Date(),
|
||
};
|
||
if (description !== undefined) {
|
||
insertData.description = description;
|
||
conflictUpdateData.description = description;
|
||
}
|
||
if (thumbnail !== undefined) {
|
||
insertData.thumbnail = thumbnail;
|
||
conflictUpdateData.thumbnail = thumbnail;
|
||
}
|
||
|
||
// 创建新画布(使用指定 ID),并兼容同一用户并发创建同一画布的场景。
|
||
const [newProject] = await db
|
||
.insert(canvasProject)
|
||
.values(insertData)
|
||
.onConflictDoUpdate({
|
||
target: canvasProject.id,
|
||
set: conflictUpdateData,
|
||
where: eq(canvasProject.userId, session.user.id),
|
||
})
|
||
.returning();
|
||
|
||
if (!newProject) {
|
||
return NextResponse.json(
|
||
{ success: false, error: '画布ID已被占用' },
|
||
{ status: 409 }
|
||
);
|
||
}
|
||
|
||
return NextResponse.json({ success: true, data: newProject });
|
||
}
|
||
} 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 });
|
||
}
|
||
}
|