150 lines
4.6 KiB
TypeScript
150 lines
4.6 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { auth } from "@/app/(auth)/auth";
|
|
import {
|
|
applyCanvasOperationPlan,
|
|
CanvasAgentMutationError,
|
|
} from "@/lib/canvas/agent-mutations";
|
|
import type { CanvasOperationPlan } from "@/types/canvas/agent-protocol";
|
|
|
|
const canvasOperationPlanSchema = z
|
|
.object({
|
|
version: z.literal(1),
|
|
operationId: z.string().min(1).max(160),
|
|
operation: z.enum([
|
|
"fill_slot",
|
|
"insert_beside",
|
|
"append_revision",
|
|
"link_nodes",
|
|
"move_nodes",
|
|
"update_text",
|
|
]),
|
|
projectId: z.string().uuid().optional(),
|
|
baseCanvasRevision: z.string().max(160).optional(),
|
|
baseCanvasUpdatedAt: z.string().max(160).optional(),
|
|
sourceNodeIds: z.array(z.string().min(1).max(160)).max(20).optional(),
|
|
targetNodeIds: z.array(z.string().min(1).max(160)).max(20).optional(),
|
|
targetSlotId: z.string().min(1).max(160).optional(),
|
|
anchorNodeId: z.string().min(1).max(160).optional(),
|
|
edgeType: z.enum(["default", "reference", "conversation"]).optional(),
|
|
textContent: z.string().min(1).max(4000).optional(),
|
|
confirmed: z.boolean().optional(),
|
|
layoutStrategy: z.enum(["row", "column", "grid"]).optional(),
|
|
spacing: z.number().positive().max(240).optional(),
|
|
targetPosition: z
|
|
.object({
|
|
x: z.number(),
|
|
y: z.number(),
|
|
})
|
|
.optional(),
|
|
placement: z
|
|
.enum(["right", "left", "below", "above", "viewport-center"])
|
|
.optional(),
|
|
placementStrategy: z
|
|
.enum(["single", "row", "grid", "overlay", "beside_each_source"])
|
|
.optional(),
|
|
preserveOriginal: z.boolean().optional(),
|
|
media: z
|
|
.object({
|
|
type: z.enum(["image", "video"]),
|
|
url: z.string().url(),
|
|
assetId: z.string().uuid().optional(),
|
|
width: z.number().positive().optional(),
|
|
height: z.number().positive().optional(),
|
|
title: z.string().max(256).optional(),
|
|
prompt: z.string().max(2000).optional(),
|
|
sourceNodeIds: z.array(z.string().min(1).max(160)).max(20).optional(),
|
|
metadata: z.record(z.unknown()).optional(),
|
|
})
|
|
.optional(),
|
|
mediaItems: z
|
|
.array(
|
|
z.object({
|
|
mediaItemId: z.string().min(1).max(160),
|
|
type: z.enum(["image", "video"]),
|
|
url: z.string().url(),
|
|
assetId: z.string().uuid().optional(),
|
|
width: z.number().positive().optional(),
|
|
height: z.number().positive().optional(),
|
|
title: z.string().max(256).optional(),
|
|
prompt: z.string().max(2000).optional(),
|
|
sourceNodeIds: z.array(z.string().min(1).max(160)).max(20).optional(),
|
|
metadata: z.record(z.unknown()).optional(),
|
|
})
|
|
)
|
|
.max(20)
|
|
.optional(),
|
|
metadata: z.record(z.unknown()).optional(),
|
|
})
|
|
.refine(
|
|
(value) =>
|
|
value.operation === "link_nodes" ||
|
|
value.operation === "update_text" ||
|
|
value.operation === "move_nodes" ||
|
|
value.media ||
|
|
(value.mediaItems?.length ?? 0) > 0,
|
|
{
|
|
message: "缺少媒体结果",
|
|
path: ["mediaItems"],
|
|
}
|
|
)
|
|
.refine(
|
|
(value) =>
|
|
value.operation !== "update_text" ||
|
|
Boolean(String(value.textContent || "").trim()),
|
|
{
|
|
message: "缺少文本内容",
|
|
path: ["textContent"],
|
|
}
|
|
);
|
|
|
|
const requestSchema = z.object({
|
|
operationPlan: canvasOperationPlanSchema,
|
|
});
|
|
|
|
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 } = await params;
|
|
const body = requestSchema.parse(await request.json());
|
|
const result = await applyCanvasOperationPlan({
|
|
userId: session.user.id,
|
|
projectId: id,
|
|
plan: body.operationPlan as CanvasOperationPlan,
|
|
});
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: result,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof CanvasAgentMutationError) {
|
|
return NextResponse.json(
|
|
{ success: false, error: error.message },
|
|
{ status: error.status }
|
|
);
|
|
}
|
|
if (error instanceof z.ZodError) {
|
|
return NextResponse.json(
|
|
{ success: false, error: "请求参数无效", details: error.flatten() },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
console.error("[CanvasAgentOperations] apply operation failed:", error);
|
|
return NextResponse.json(
|
|
{ success: false, error: "执行画布操作失败" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|