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 }
);
}
}