fix: anchor canvas writeback to resolved targets

This commit is contained in:
134 2026-06-23 16:02:41 +08:00
parent e73c2da84e
commit ef0f9bc93d
2 changed files with 98 additions and 5 deletions

View File

@ -5,6 +5,7 @@ import {
} from "@/lib/canvas/agent-context";
import type {
CanvasAgentContext,
CanvasAgentNodeRef,
CanvasMediaItem,
CanvasOperationKind,
CanvasOperationPlan,
@ -46,6 +47,28 @@ export interface CanvasAgentDirectWritebackPlanResult {
selectedMediaNodes: ReturnType<typeof getSelectedMediaNodes>;
}
function buildKnownNodeMap(context: CanvasAgentContext) {
return new Map<string, CanvasAgentNodeRef>(
[...context.candidateNodes, ...context.selectedNodes].map((node) => [
node.nodeId,
node,
])
);
}
function resolveTargetMediaNodes(params: {
context: CanvasAgentContext;
targetNodeIds: string[];
}) {
const knownNodes = buildKnownNodeMap(params.context);
return params.targetNodeIds
.map((nodeId) => knownNodes.get(nodeId))
.filter(
(node): node is CanvasAgentNodeRef =>
node !== undefined && (node.type === "image" || node.type === "video")
);
}
export function buildCanvasAgentDirectWritebackPlan(
params: BuildCanvasAgentDirectWritebackPlanParams
): CanvasAgentDirectWritebackPlanResult | null {
@ -62,17 +85,26 @@ export function buildCanvasAgentDirectWritebackPlan(
const targetSlotId = preferredTargetSlotId || selectedSlot?.nodeId || "";
const shouldFillSlot = Boolean(targetSlotId);
const selectedMediaNodes = getSelectedMediaNodes(params.context);
const anchorNode =
selectedSlot || selectedMediaNodes[0] || getBestAnchorNode(params.context);
const targetRefs = params.targetRefs || [];
const targetNodeIds = Array.from(
new Set(
targetRefs
.map((ref) => ref.nodeId)
.map((ref) => String(ref.nodeId || "").trim())
.filter((value): value is string => Boolean(value))
)
);
const selectedMediaNodeIds = selectedMediaNodes
const targetMediaNodes = resolveTargetMediaNodes({
context: params.context,
targetNodeIds,
});
const writebackMediaNodes =
targetMediaNodes.length > 0 ? targetMediaNodes : selectedMediaNodes;
const anchorNode =
selectedSlot ||
targetMediaNodes[0] ||
selectedMediaNodes[0] ||
getBestAnchorNode(params.context);
const selectedMediaNodeIds = writebackMediaNodes
.map((node) => node.nodeId)
.filter((value): value is string => Boolean(value));
const perResultSourceNodeIds =
@ -154,6 +186,6 @@ export function buildCanvasAgentDirectWritebackPlan(
operation,
targetSlotId,
shouldFillSlot,
selectedMediaNodes,
selectedMediaNodes: writebackMediaNodes,
};
}

View File

@ -509,6 +509,67 @@ test.describe("canvas chat pure helpers", () => {
});
});
test("anchors direct image edits to resolved target refs before fallback selection", () => {
const context: CanvasAgentContext = {
...plannerContext,
selectedNodeIds: [],
selectedNodes: [],
candidateNodes: [
{
nodeId: "image-left",
type: "image",
title: "左图",
x: 0,
y: 0,
width: 240,
height: 180,
},
{
nodeId: "image-right",
type: "image",
title: "右图",
x: 420,
y: 0,
width: 240,
height: 180,
},
],
focusPolicy: "auto",
};
const result = buildCanvasAgentDirectWritebackPlan({
projectId: plannerContext.projectId || "",
chatId: "chat-1",
assistantMessageId: "assistant-targeted",
operationLabel: "agent-edit-image",
resultType: "image",
resultUrls: ["https://cdn.example.com/right-revision.png"],
prompt: "把右边那张背景换成白色",
mode: "edit",
context,
targetRefs: [{ nodeId: " image-right " }],
sourceImageUrls: ["https://cdn.example.com/right-source.png"],
});
expect(result).toMatchObject({
operation: "append_revision",
plan: {
operation: "append_revision",
anchorNodeId: "image-right",
sourceNodeIds: ["image-right"],
},
});
expect(result?.selectedMediaNodes.map((node) => node.nodeId)).toEqual([
"image-right",
]);
expect(result?.mediaItems[0]).toMatchObject({
sourceNodeIds: ["image-right"],
metadata: {
sourceImageUrls: ["https://cdn.example.com/right-source.png"],
},
});
});
test("plans direct batch generations beside each matched source", () => {
const context: CanvasAgentContext = {
...plannerContext,