ai-chatbot/lib/canvas/agent-operation-request.ts
2026-06-23 16:27:06 +08:00

179 lines
5.5 KiB
TypeScript

import { z } from "zod";
import type { CanvasOperationPlan } from "@/types/canvas/agent-protocol";
export const canvasAgentOperationPlanSchema = z
.object({
version: z.literal(1),
operationId: z.string().min(1).max(160),
operation: z.enum([
"create_agent_slot",
"fill_slot",
"insert_beside",
"append_revision",
"replace_node_media",
"add_annotation",
"link_nodes",
"move_nodes",
"delete_nodes",
"rename_node",
"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: 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 === "create_agent_slot" ||
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.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 !== "rename_node" ||
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;
}