import type { CanvasCapabilityClientCommand } from "@/lib/canvas/capabilities/client-command"; import { createCanvasCapabilityClientCommand } from "@/lib/canvas/capabilities/client-command"; import { isCanvasCapabilityEnabled } from "@/lib/canvas/capabilities/availability"; import { CanvasCapabilityRuntimeError, resolveCanvasCapabilityContext, } from "@/lib/canvas/capabilities/context"; import { shouldExecuteCanvasCapabilityHeadlessly } from "@/lib/canvas/capabilities/execution-mode"; import { executeCanvasCapabilityHeadlessly, type CanvasCapabilityMediaResult, } from "@/lib/canvas/capabilities/headless-executor"; import { getCanvasCapability } from "@/lib/canvas/capabilities/registry"; import { enqueueCanvasCapabilityExecution } from "@/lib/canvas/capabilities/task-queue"; import { createOrRecoverCanvasCapabilityTask, MAX_CANVAS_CAPABILITY_TASK_ATTEMPTS, markCanvasCapabilityTaskFailed, markCanvasCapabilityTaskRunning, markCanvasCapabilityTaskSucceeded, } from "@/lib/canvas/capabilities/task-store"; import { applyCanvasOperationPlan } from "@/lib/canvas/agent-mutations"; import type { CanvasOperationPlan } from "@/types/canvas/agent-protocol"; export type CanvasCapabilityRuntimeResult = { clientCommand?: CanvasCapabilityClientCommand; mode: "headless" | "interactive"; operation?: { canvasRevision: string; canvasUpdatedAt: string; createdNodeIds: string[]; createdNodeIdsByMediaItemId: Record; updatedNodeIds: string[]; }; outputs?: Array<{ assetId?: string; sourceNodeId: string; title: string; url: string; }>; task: { attempts: number; error: string | null; id: string; requestId: string; result: unknown; status: string; version: number; }; }; function serializeTask(task: { attempts: number; error: string | null; id: string; requestId: string; result: unknown; status: string; version: number; }) { return { attempts: task.attempts, error: task.error, id: task.id, requestId: task.requestId, result: task.result, status: task.status, version: task.version, }; } function buildMediaOperationPlan(params: { command: CanvasCapabilityClientCommand; mediaResults: CanvasCapabilityMediaResult[]; projectId: string; projectRevision: string; }) { return { version: 1, operationId: params.command.requestId, operation: "insert_beside", projectId: params.projectId, baseCanvasRevision: params.projectRevision, sourceNodeIds: params.command.nodeIds, targetNodeIds: params.command.nodeIds, placement: "right", placementStrategy: "beside_each_source", preserveOriginal: true, layoutStrategy: params.mediaResults.length > 4 ? "grid" : "row", spacing: 24, mediaItems: params.mediaResults.map((item, index) => ({ mediaItemId: `${params.command.requestId}:result-${index + 1}`, type: item.nodeType || "image", url: item.url, assetId: item.assetId, width: item.width, height: item.height, title: item.title, prompt: item.prompt, sourceNodeIds: [item.sourceNodeId], metadata: { ...(item.metadata || {}), canvasCapabilityId: params.command.capabilityId, canvasCapabilityRequestId: params.command.requestId, canvasCapabilitySource: params.command.source, sourceNodeId: item.sourceNodeId, }, })), metadata: { canvasCapabilityId: params.command.capabilityId, canvasCapabilityRequestId: params.command.requestId, canvasCapabilitySource: params.command.source, canvasCapabilityVersion: params.command.version, }, } satisfies CanvasOperationPlan; } function buildMutationOperationPlan(params: { command: CanvasCapabilityClientCommand; projectId: string; projectRevision: string; }): CanvasOperationPlan | null { const common = { version: 1 as const, operationId: params.command.requestId, projectId: params.projectId, baseCanvasRevision: params.projectRevision, targetNodeIds: params.command.nodeIds, sourceNodeIds: params.command.nodeIds, metadata: { canvasCapabilityId: params.command.capabilityId, canvasCapabilityRequestId: params.command.requestId, canvasCapabilitySource: params.command.source, }, }; if (params.command.capabilityId === "image.rename") { return { ...common, operation: "rename_node", textContent: String(params.command.args.title || "").trim(), }; } if (params.command.capabilityId === "image.note") { return { ...common, operation: "update_note", textContent: String(params.command.args.text || "").trim(), }; } if (params.command.capabilityId === "image.delete") { return { ...common, operation: "delete_nodes", confirmed: params.command.confirmed === true, }; } return null; } function summarizeOperation(operation: Awaited>) { return { canvasRevision: operation.canvasRevision, canvasUpdatedAt: operation.canvasUpdatedAt, createdNodeIds: operation.createdNodeIds, createdNodeIdsByMediaItemId: operation.createdNodeIdsByMediaItemId, updatedNodeIds: operation.updatedNodeIds, }; } export async function executeCanvasCapabilityRuntime(params: { command: CanvasCapabilityClientCommand; projectId: string; requestHeaders: Headers; requestUrl: string; userId: string; }): Promise { const command = createCanvasCapabilityClientCommand({ capabilityId: params.command.capabilityId, nodeIds: params.command.nodeIds, projectId: params.projectId, requestId: params.command.requestId, args: params.command.args, source: params.command.source, confirmed: params.command.confirmed, createdAt: params.command.createdAt, }); const capability = getCanvasCapability(command.capabilityId); if (!capability) { throw new CanvasCapabilityRuntimeError("画布能力不存在", 404); } if (!(await isCanvasCapabilityEnabled(capability))) { throw new CanvasCapabilityRuntimeError( `${capability.title}已在系统设置中关闭`, 403, "CANVAS_CAPABILITY_DISABLED" ); } const context = await resolveCanvasCapabilityContext({ command, projectId: params.projectId, requiresRemoteImage: capability.requiresRemoteImage, userId: params.userId, }); let task = await createOrRecoverCanvasCapabilityTask({ command, userId: params.userId, projectId: params.projectId, }); if (task.status === "succeeded") { const storedResult = task.result && typeof task.result === "object" ? (task.result as Omit) : ({ mode: "headless" } as const); return { ...storedResult, task: serializeTask(task) }; } if (task.status === "running") { return { mode: shouldExecuteCanvasCapabilityHeadlessly({ capability, command }) ? "headless" : "interactive", task: serializeTask(task), }; } if ( task.status === "failed" && task.attempts >= MAX_CANVAS_CAPABILITY_TASK_ATTEMPTS ) { throw new CanvasCapabilityRuntimeError( task.error || "画布能力任务超过最大重试次数", 409, "CANVAS_CAPABILITY_RETRY_EXHAUSTED" ); } const shouldRunHeadlessly = shouldExecuteCanvasCapabilityHeadlessly({ capability, command, }); if (!shouldRunHeadlessly) { task = await markCanvasCapabilityTaskRunning(task.id); const result = { mode: "interactive" as const, clientCommand: command, }; task = (await markCanvasCapabilityTaskSucceeded(task.id, result)) || task; return { ...result, task: serializeTask(task) }; } return enqueueCanvasCapabilityExecution({ projectId: params.projectId, execute: async () => { task = await markCanvasCapabilityTaskRunning(task.id); try { const projectRevision = context.project.updatedAt.toISOString(); const mutationPlan = buildMutationOperationPlan({ command, projectId: params.projectId, projectRevision, }); const mediaResults = mutationPlan ? [] : await executeCanvasCapabilityHeadlessly({ command, requestHeaders: params.requestHeaders, requestUrl: params.requestUrl, targets: context.targets, userId: params.userId, }); const operation = await applyCanvasOperationPlan({ userId: params.userId, projectId: params.projectId, plan: mutationPlan || buildMediaOperationPlan({ command, mediaResults, projectId: params.projectId, projectRevision, }), }); const result = { mode: "headless" as const, operation: summarizeOperation(operation), outputs: mediaResults.map((item) => ({ assetId: item.assetId, sourceNodeId: item.sourceNodeId, title: item.title, url: item.url, })), }; task = (await markCanvasCapabilityTaskSucceeded(task.id, result)) || task; return { ...result, task: serializeTask(task) }; } catch (error) { task = (await markCanvasCapabilityTaskFailed(task.id, error)) || task; throw error; } }, }); }