import type { CanvasAgentContext, CanvasOperationPlan, } from "@/types/canvas/agent-protocol"; import { buildCanvasAgentSlotCreationPlan, resolveCanvasAgentSlotCreationIntent, } from "@/lib/canvas/agent-model-planner"; type ExplicitOperationKind = | "add_annotation" | "create_agent_slot" | "create_text_node" | "link_nodes" | "rename_node" | "update_text" | "update_note"; export type CanvasAgentExplicitOperationPlan = { operationPlan: CanvasOperationPlan; reply: string; progressTitle: string; progressMessage: string; completedTitle: string; failedTitle: string; }; function trimString(value: unknown, maxLength: number) { return typeof value === "string" ? value.trim().slice(0, maxLength) : ""; } function normalizeNodeIds(values: Array, maxItems = 20) { return Array.from( new Set(values.map((value) => trimString(value, 160)).filter(Boolean)) ).slice(0, maxItems); } function stripCommandContent(value: unknown, maxLength: number) { return trimString(value, maxLength) .replace(/^["'“”‘’]+|["'“”‘’。;;]+$/g, "") .trim() .slice(0, maxLength); } function getKnownNodeTypes(context: CanvasAgentContext) { const nodeMap = new Map(); [...context.candidateNodes, ...context.selectedNodes].forEach((node) => { if (node.nodeId) nodeMap.set(node.nodeId, node.type); }); context.slots.forEach((slot) => { if (slot.nodeId) nodeMap.set(slot.nodeId, "agent-slot"); }); return nodeMap; } function getSelectedNodeIds(context: CanvasAgentContext) { return normalizeNodeIds([ ...context.selectedNodeIds, ...context.selectedNodes.map((node) => node.nodeId), ...context.slots .filter((slot) => slot.isSelected || context.selectedNodeIds.includes(slot.nodeId)) .map((slot) => slot.nodeId), ]); } function getTargetNodeIds(params: { context: CanvasAgentContext; requestedNodeIds?: string[]; allowedTypes?: string[]; }) { const knownNodeTypes = getKnownNodeTypes(params.context); const allowedTypes = params.allowedTypes ? new Set(params.allowedTypes) : null; return normalizeNodeIds([ ...(params.requestedNodeIds || []), ...getSelectedNodeIds(params.context), ]).filter((nodeId) => { const type = knownNodeTypes.get(nodeId); if (!type) return false; return allowedTypes ? allowedTypes.has(type) : true; }); } function getRenameAllowedTypes(userText: string) { const lowerText = userText.toLowerCase(); if (/槽位|slot/.test(lowerText)) return ["agent-slot"]; if (/视频|video/.test(lowerText)) return ["video"]; if (/图片|图像|照片|主图|参考图|image/.test(lowerText)) return ["image"]; if (/文本|文字|文案|text/.test(lowerText)) return ["text"]; return undefined; } function hasMediaMutationIntent(userText: string) { return /生成|出图|做一版|再出|换|改图|编辑图片|编辑图|抠图|去水印|高清|清晰|扩图|裁剪|删除|替换|背景|模型|洗图|重绘|尺寸|像素|分辨率/.test( userText.toLowerCase() ); } function extractContentFromPatterns(userText: string, patterns: RegExp[], maxLength: number) { for (const pattern of patterns) { const match = userText.match(pattern); const content = stripCommandContent(match?.[1] || "", maxLength); if (content) return content; } return ""; } function getViewportCenteredPosition(params: { context: CanvasAgentContext; size: { width: number; height: number }; }) { const viewport = params.context.viewport || { x: 0, y: 0, zoom: 1 }; const zoom = Math.max(0.1, Number(viewport.zoom || 1)); const viewportWidth = Math.max(320, Number(viewport.width || 1200)); const viewportHeight = Math.max(320, Number(viewport.height || 800)); return { x: (viewportWidth / 2 - Number(viewport.x || 0)) / zoom - params.size.width / 2, y: (viewportHeight / 2 - Number(viewport.y || 0)) / zoom - params.size.height / 2, }; } function buildPlanBase(params: { context: CanvasAgentContext; projectId: string; messageId: string; chatId: string; userText: string; operation: ExplicitOperationKind; }): Pick< CanvasOperationPlan, | "version" | "operationId" | "operation" | "projectId" | "baseCanvasRevision" | "baseCanvasUpdatedAt" | "preserveOriginal" | "metadata" > { return { version: 1, operationId: `${params.messageId}:canvas-${params.operation}`, operation: params.operation, projectId: params.projectId, baseCanvasRevision: params.context.canvasRevision, baseCanvasUpdatedAt: params.context.canvasUpdatedAt, preserveOriginal: true, metadata: { messageId: params.messageId, chatId: params.chatId, planner: "deterministic", prompt: params.userText, }, }; } function buildCreateTextOperation(params: { context: CanvasAgentContext; projectId: string; messageId: string; chatId: string; userText: string; }): CanvasAgentExplicitOperationPlan | null { const text = params.userText.trim(); const hasCreateTextIntent = /(?:创建|新增|新建|添加|加一段|加一句|写一段|写一句|放一段|放一句|写入|写上|放上).*(?:文本|文字|文案|标题|标语|说明|caption|copy|text)|(?:文本|文字|文案|标题|标语|说明).*(?:创建|新增|新建|添加|写入|写上)/i.test( text ); if (!hasCreateTextIntent || hasMediaMutationIntent(text)) return null; const textContent = extractContentFromPatterns( text, [ /(?:文本|文字|文案|标题|标语|说明|caption|copy|text)[^::]*[::]\s*([\s\S]+)$/i, /(?:写入|写上|写成|内容是|设为|设置为)\s*[::]?\s*([\s\S]+)$/i, /[::]\s*([\s\S]+)$/i, ], 4000 ); if (!textContent) return null; const textSize = { width: 320, height: 120 }; const basePlan = buildPlanBase({ ...params, operation: "create_text_node" }); return { operationPlan: { ...basePlan, targetPosition: getViewportCenteredPosition({ context: params.context, size: textSize, }), textContent, metadata: { ...basePlan.metadata, canvasAgentViewport: params.context.viewport, }, }, reply: "已在当前画布添加文本节点。", progressTitle: "正在添加画布文本", progressMessage: "我会在当前视口中心创建一个新的文本节点。", completedTitle: "画布文本已添加", failedTitle: "画布文本添加失败", }; } function hasImageTextEditIntent(userText: string) { return /(?:图片|图像|照片|主图|海报|画面|图中|图里|图片里|图片上|海报上).*(?:文字|文本|文案|字).*(?:改|换|替换|编辑|修改|去掉|删除)|(?:文字|文本|文案|字).*(?:图片|图像|照片|主图|海报|画面|图中|图里|图片里|图片上|海报上).*(?:改|换|替换|编辑|修改|去掉|删除)/.test( userText ); } function buildUpdateTextOperation(params: { context: CanvasAgentContext; projectId: string; messageId: string; chatId: string; userText: string; requestedNodeIds?: string[]; }): CanvasAgentExplicitOperationPlan | null { const text = params.userText.trim(); const hasUpdateTextIntent = /(?:选中|当前|这个|这段|该).*(?:文本|文字|文案|标语|说明|copy|text).*(?:改成|改为|修改为|更新为|设为|设置为|换成)|(?:文本|文字|文案|标语|说明|copy|text).*(?:改成|改为|修改为|更新为|设为|设置为|换成)/i.test( text ); if (!hasUpdateTextIntent || hasMediaMutationIntent(text)) return null; if (hasImageTextEditIntent(text)) return null; const textContent = extractContentFromPatterns( text, [ /(?:改成|改为|修改为|更新为|设为|设置为|换成)\s*[::]?\s*([\s\S]+)$/i, /(?:文本|文字|文案|标语|说明|copy|text)[^::]*(?:改成|改为|修改为|更新为|设为|设置为|换成)\s*[::]?\s*([\s\S]+)$/i, /[::]\s*([\s\S]+)$/i, ], 4000 ); if (!textContent) return null; const targetNodeIds = getTargetNodeIds({ context: params.context, requestedNodeIds: params.requestedNodeIds, allowedTypes: ["text"], }).slice(0, 10); if (targetNodeIds.length === 0) return null; return { operationPlan: { ...buildPlanBase({ ...params, operation: "update_text" }), targetNodeIds, textContent, }, reply: "已更新选中文本内容。", progressTitle: "正在更新画布文本", progressMessage: "我会只修改选中文本节点的内容,不移动画布节点。", completedTitle: "画布文本已更新", failedTitle: "画布文本更新失败", }; } function buildLinkNodesOperation(params: { context: CanvasAgentContext; projectId: string; messageId: string; chatId: string; userText: string; requestedNodeIds?: string[]; }): CanvasAgentExplicitOperationPlan | null { const text = params.userText.trim(); const hasLinkIntent = /连起来|连线|连接|关联|参考关系|版本关系|建立关系/.test(text); if (!hasLinkIntent || hasMediaMutationIntent(text)) return null; const targetNodeIds = getTargetNodeIds({ context: params.context, requestedNodeIds: params.requestedNodeIds, }).slice(0, 20); if (targetNodeIds.length < 2) return null; return { operationPlan: { ...buildPlanBase({ ...params, operation: "link_nodes" }), sourceNodeIds: targetNodeIds, edgeType: "reference", }, reply: "已为选中的画布节点建立参考连线。", progressTitle: "正在连接画布节点", progressMessage: "我会给选中的画布节点建立参考关系,不修改节点内容。", completedTitle: "画布节点已连接", failedTitle: "画布节点连接失败", }; } function getSlotTypeLabel(slotType: "image" | "video" | "ui-screen") { if (slotType === "video") return "视频"; if (slotType === "ui-screen") return "UI"; return "图片"; } function buildCreateAgentSlotOperation(params: { context: CanvasAgentContext; projectId: string; messageId: string; chatId: string; userText: string; }): CanvasAgentExplicitOperationPlan | null { const intent = resolveCanvasAgentSlotCreationIntent({ userText: params.userText, }); if (!intent.shouldCreate || intent.combinedWithMediaExecution) return null; const operationPlan = buildCanvasAgentSlotCreationPlan({ ...params, slotType: intent.slotType, slotLabel: intent.label, aspectRatio: intent.aspectRatio, }); if (!operationPlan) return null; const slotTypeLabel = getSlotTypeLabel(intent.slotType); return { operationPlan: { ...operationPlan, metadata: { ...(operationPlan.metadata || {}), planner: "deterministic", }, }, reply: `已在当前画布创建${slotTypeLabel}槽位。`, progressTitle: "正在创建画布槽位", progressMessage: `我会在当前视口中心创建一个空${slotTypeLabel}槽位。`, completedTitle: "画布槽位已创建", failedTitle: "画布槽位创建失败", }; } function buildRenameOperation(params: { context: CanvasAgentContext; projectId: string; messageId: string; chatId: string; userText: string; requestedNodeIds?: string[]; }): CanvasAgentExplicitOperationPlan | null { const text = params.userText.trim(); const hasRenameIntent = /改名|重命名|命名为|标题(?:改成|改为|设为|设置为)|改标题|修改标题/.test( text ); if (!hasRenameIntent || hasMediaMutationIntent(text)) return null; const title = extractContentFromPatterns( text, [ /(?:改名为|重命名为|命名为|标题(?:改成|改为|设为|设置为)|改标题为|修改标题为)\s*[::]?\s*([\s\S]+)$/i, /[::]\s*([\s\S]+)$/i, ], 120 ); if (!title) return null; const targetNodeIds = getTargetNodeIds({ context: params.context, requestedNodeIds: params.requestedNodeIds, allowedTypes: getRenameAllowedTypes(text), }).slice(0, 10); if (targetNodeIds.length === 0) return null; return { operationPlan: { ...buildPlanBase({ ...params, operation: "rename_node" }), targetNodeIds, textContent: title, }, reply: "已更新选中节点标题。", progressTitle: "正在更新节点标题", progressMessage: "我会只修改选中节点的标题,不改变媒体内容。", completedTitle: "节点标题已更新", failedTitle: "节点标题更新失败", }; } function buildUpdateNoteOperation(params: { context: CanvasAgentContext; projectId: string; messageId: string; chatId: string; userText: string; requestedNodeIds?: string[]; }): CanvasAgentExplicitOperationPlan | null { const text = params.userText.trim(); if (!/备注|note/i.test(text)) return null; if (/按.*(?:备注|note).*(?:修改|编辑|改图|改主图|生成|出图)/i.test(text)) { return null; } if (hasMediaMutationIntent(text)) return null; const note = extractContentFromPatterns( text, [ /(?:备注为|备注成|添加备注|加备注|写备注|备注|note)\s*[::]?\s*([\s\S]+)$/i, /[::]\s*([\s\S]+)$/i, ], 1000 ); if (!note) return null; const targetNodeIds = getTargetNodeIds({ context: params.context, requestedNodeIds: params.requestedNodeIds, allowedTypes: ["image", "video"], }).slice(0, 10); if (targetNodeIds.length === 0) return null; return { operationPlan: { ...buildPlanBase({ ...params, operation: "update_note" }), targetNodeIds, textContent: note, }, reply: "已更新选中图片备注。", progressTitle: "正在更新图片备注", progressMessage: "我会把备注保存到选中的图片或视频节点。", completedTitle: "图片备注已更新", failedTitle: "图片备注更新失败", }; } function buildAddAnnotationOperation(params: { context: CanvasAgentContext; projectId: string; messageId: string; chatId: string; userText: string; requestedNodeIds?: string[]; }): CanvasAgentExplicitOperationPlan | null { const text = params.userText.trim(); if (!/批注|标注|箭头|框选/i.test(text)) return null; if (/按.*(?:批注|标注|箭头|框选).*(?:修改|编辑|改图|改主图|生成|出图|修图)/i.test(text)) { return null; } if (hasMediaMutationIntent(text)) return null; const annotationText = extractContentFromPatterns( text, [ /(?:添加批注|加批注|写批注|批注为|批注成|批注|添加标注|加标注|写标注|标注为|标注成|标注|箭头说明|框选说明)\s*[::]?\s*([\s\S]+)$/i, /[::]\s*([\s\S]+)$/i, ], 1000 ); if (!annotationText) return null; const targetNodeIds = getTargetNodeIds({ context: params.context, requestedNodeIds: params.requestedNodeIds, allowedTypes: ["image", "video"], }).slice(0, 10); if (targetNodeIds.length === 0) return null; return { operationPlan: { ...buildPlanBase({ ...params, operation: "add_annotation" }), targetNodeIds, textContent: annotationText, annotations: [ { type: "text-note", text: annotationText, color: "#f59e0b", }, ], }, reply: "已给选中图片添加批注。", progressTitle: "正在添加画布批注", progressMessage: "我会把批注保存到选中的图片或视频节点。", completedTitle: "画布批注已添加", failedTitle: "画布批注添加失败", }; } export function buildCanvasAgentExplicitOperationPlan(params: { context: CanvasAgentContext; projectId: string; messageId: string; chatId: string; userText: string; requestedNodeIds?: string[]; }): CanvasAgentExplicitOperationPlan | null { const commonParams = { ...params, projectId: trimString(params.projectId, 160), messageId: trimString(params.messageId, 160), chatId: trimString(params.chatId, 160), }; if (!commonParams.projectId || !commonParams.messageId || !commonParams.chatId) { return null; } return ( buildCreateAgentSlotOperation(commonParams) || buildCreateTextOperation(commonParams) || buildUpdateTextOperation(commonParams) || buildLinkNodesOperation(commonParams) || buildRenameOperation(commonParams) || buildUpdateNoteOperation(commonParams) || buildAddAnnotationOperation(commonParams) ); }