import { z } from "zod"; import type { CanvasOperationPlan } from "@/types/canvas/agent-protocol"; const httpMediaUrlSchema = z .string() .url() .refine((url) => /^https?:\/\//i.test(url), { message: "仅支持 http/https 媒体 URL", }); export const canvasAgentOperationPlanSchema = z .object({ version: z.literal(1), operationId: z.string().min(1).max(160), operation: z.enum([ "create_agent_slot", "create_text_node", "fill_slot", "insert_beside", "append_revision", "replace_node_media", "add_annotation", "link_nodes", "move_nodes", "delete_nodes", "rename_node", "update_note", "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(), slot: z .object({ slotType: z.enum(["image", "video", "ui-screen"]), label: z.string().max(120).optional(), aspectRatio: z.string().max(32).optional(), fillPolicy: z .enum(["replace-empty", "append-version", "replace-existing"]) .optional(), acceptedSources: z .array(z.enum(["text", "image", "selection", "annotation"])) .max(4) .optional(), }) .optional(), annotations: z .array( z.object({ id: z.string().min(1).max(128).optional(), sourceNodeId: z.string().min(1).max(160).optional(), type: z.enum(["text-note", "arrow-note", "box", "freehand", "crop-box"]), text: z.string().max(1000).optional(), bounds: z .object({ x: z.number(), y: z.number(), width: z.number(), height: z.number(), }) .optional(), targetPoint: z .object({ x: z.number(), y: z.number(), }) .optional(), color: z.string().max(64).optional(), createdAt: z.string().max(80).optional(), }) ) .max(20) .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: httpMediaUrlSchema, 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: httpMediaUrlSchema, 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 === "create_agent_slot" || value.operation === "create_text_node" || value.operation === "link_nodes" || value.operation === "update_text" || value.operation === "add_annotation" || value.operation === "move_nodes" || value.operation === "delete_nodes" || value.operation === "rename_node" || value.operation === "update_note" || value.media || (value.mediaItems?.length ?? 0) > 0, { message: "缺少媒体结果", path: ["mediaItems"], } ) .refine( (value) => value.operation !== "add_annotation" || Boolean(String(value.textContent || "").trim()) || (value.annotations?.length ?? 0) > 0, { message: "缺少批注内容", path: ["annotations"], } ) .refine( (value) => value.operation !== "update_text" || Boolean(String(value.textContent || "").trim()), { message: "缺少文本内容", path: ["textContent"], } ) .refine( (value) => value.operation !== "create_text_node" || Boolean(String(value.textContent || "").trim()), { message: "缺少文本内容", path: ["textContent"], } ) .refine( (value) => value.operation !== "rename_node" || Boolean(String(value.textContent || "").trim()), { message: "缺少节点标题", path: ["textContent"], } ) .refine( (value) => value.operation !== "update_note" || Boolean(String(value.textContent || "").trim()), { message: "缺少备注内容", path: ["textContent"], } ); export const canvasAgentOperationRequestSchema = z.object({ operationPlan: canvasAgentOperationPlanSchema, }); export type CanvasAgentOperationRequestBody = { operationPlan: CanvasOperationPlan; }; export function parseCanvasAgentOperationRequestBody( value: unknown ): CanvasAgentOperationRequestBody { return canvasAgentOperationRequestSchema.parse( value ) as CanvasAgentOperationRequestBody; }