diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index f7c255a12..0791684c2 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -4064,6 +4064,55 @@ export async function POST(request: Request) { const canvasExplicitOperationPlan = resolveCanvasExplicitOperationPlan(); if (canvasExplicitOperationPlan) { const { operationPlan } = canvasExplicitOperationPlan; + if ( + operationPlan.operation === "create_agent_slot" && + !canvasAgentFeatures.slotEnabled + ) { + const canvasSlotDisabledStream = createUIMessageStream({ + execute: async ({ writer }) => { + const messageId = generateUUID(); + const slotDisabledReply = "当前画布 Agent Slot 能力暂未开启。"; + writeAgentProgressChunk( + writer, + { + stage: "completed", + currentStep: "finalize", + title: "画布槽位未创建", + message: slotDisabledReply, + toolName: "canvasOperation", + capability: operationPlan.operation, + }, + "canvas-explicit:slot-disabled", + ); + writeAssistantTextChunk( + writer, + slotDisabledReply, + "canvas-explicit:slot-disabled-reply", + messageId, + ); + await saveMessages({ + messages: [ + { + id: messageId, + chatId: id, + role: "assistant", + parts: [{ type: "text", text: slotDisabledReply }], + attachments: [], + createdAt: new Date(), + videoStorageStatus: null, + temporaryVideoUrl: null, + permanentVideoUrl: null, + archivedAt: null, + videoMetadata: null, + }, + ], + }); + }, + }); + + return createSseResponse(canvasSlotDisabledStream); + } + const canvasExplicitStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); diff --git a/lib/canvas/agent-explicit-operations.ts b/lib/canvas/agent-explicit-operations.ts index 7a8432b47..03c51e669 100644 --- a/lib/canvas/agent-explicit-operations.ts +++ b/lib/canvas/agent-explicit-operations.ts @@ -2,8 +2,16 @@ import type { CanvasAgentContext, CanvasOperationPlan, } from "@/types/canvas/agent-protocol"; +import { + buildCanvasAgentSlotCreationPlan, + resolveCanvasAgentSlotCreationIntent, +} from "@/lib/canvas/agent-model-planner"; -type ExplicitOperationKind = "create_text_node" | "rename_node" | "update_note"; +type ExplicitOperationKind = + | "create_agent_slot" + | "create_text_node" + | "rename_node" + | "update_note"; export type CanvasAgentExplicitOperationPlan = { operationPlan: CanvasOperationPlan; @@ -190,6 +198,49 @@ function buildCreateTextOperation(params: { }; } +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; @@ -300,6 +351,7 @@ export function buildCanvasAgentExplicitOperationPlan(params: { } return ( + buildCreateAgentSlotOperation(commonParams) || buildCreateTextOperation(commonParams) || buildRenameOperation(commonParams) || buildUpdateNoteOperation(commonParams) diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index 505f2ca6f..1699b4488 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -944,6 +944,40 @@ test.describe("canvas chat pure helpers", () => { }); }); + test("builds deterministic agent slot creation plans from explicit canvas prompts", () => { + const context: CanvasAgentContext = { + ...plannerContext, + viewport: { x: 0, y: 0, zoom: 1, width: 1000, height: 800 }, + focusPolicy: "auto", + }; + const outcome = buildCanvasAgentExplicitOperationPlan({ + context, + projectId: plannerContext.projectId || "", + messageId: "message-1", + chatId: "chat-1", + userText: "新建一个 9:16 视频槽位", + }); + + expect(outcome?.operationPlan).toMatchObject({ + operation: "create_agent_slot", + targetPosition: { x: 320, y: 290 }, + slot: { + slotType: "video", + label: "视频槽位", + aspectRatio: "9:16", + fillPolicy: "replace-empty", + acceptedSources: ["text", "image", "selection", "annotation"], + }, + preserveOriginal: true, + metadata: { + messageId: "message-1", + chatId: "chat-1", + planner: "deterministic", + }, + }); + expect(outcome?.reply).toContain("视频槽位"); + }); + test("builds deterministic node rename plans without treating titles as image edits", () => { const outcome = buildCanvasAgentExplicitOperationPlan({ context: plannerContext, @@ -1006,6 +1040,16 @@ test.describe("canvas chat pure helpers", () => { userText: "生成一张图,标题文字:新品上市", }) ).toBeNull(); + + expect( + buildCanvasAgentExplicitOperationPlan({ + context: plannerContext, + projectId: plannerContext.projectId || "", + messageId: "message-1", + chatId: "chat-1", + userText: "新建一个视频槽位并生成片头", + }) + ).toBeNull(); }); test("normalizes model-planned annotations to selected media nodes", () => {