fix: keep annotation edit target in canvas agent context

This commit is contained in:
134 2026-06-23 15:09:50 +08:00
parent c0550988e0
commit 8ee7eb1f0a
3 changed files with 195 additions and 3 deletions

View File

@ -132,6 +132,10 @@ import {
shouldRetryCanvasViewOperationAfterRefresh,
shouldSkipCanvasMediaPayloadForHandledItems,
} from "@/components/canvas/chat-panel/canvas-agent-stream";
import {
mergePendingCanvasAgentTargetNodeIds,
resolvePendingCanvasAgentTargetNode,
} from "@/components/canvas/chat-panel/canvas-agent-targets";
import {
type DetailArchive,
type DetailBriefForm,
@ -1252,6 +1256,10 @@ export function CanvasChatPanel({
const handledCanvasMediaItemIdsRef = useRef<Set<string>>(new Set());
const pendingCanvasViewOperationRef =
useRef<CanvasViewOperationStreamPayload | null>(null);
const pendingCanvasAgentAnnotationEditTargetRef = useRef<{
nodeId: string;
requestedAt: number;
} | null>(null);
const lastWorkflowAutofillSignatureRef = useRef<string | null>(null);
const lastWorkflowAutofillUrlsRef = useRef<string[]>([]);
const pendingLaunchRef = useRef<CanvasLaunchPayload | null>(null);
@ -3626,6 +3634,13 @@ export function CanvasChatPanel({
annotationCount?: number;
}>
).detail;
const nodeId = String(detail?.nodeId || "").trim();
if (nodeId) {
pendingCanvasAgentAnnotationEditTargetRef.current = {
nodeId,
requestedAt: Date.now(),
};
}
const title = String(detail?.title || "").trim();
const prompt = title
? `按「${title}」这张图上的批注修改`
@ -4145,6 +4160,7 @@ export function CanvasChatPanel({
hiddenPromptRef.current = null;
autoContextRef.current = null;
contextPayloadRef.current = null;
pendingCanvasAgentAnnotationEditTargetRef.current = null;
return {
body: {
id: rawId,
@ -5638,7 +5654,25 @@ export function CanvasChatPanel({
}, [nodes, selectedImageNodes]);
const buildContextPayload = useCallback(() => {
const selectedNodeIdSet = new Set(selectedNodeIds);
const pendingAnnotationTargetNode = resolvePendingCanvasAgentTargetNode({
nodes,
pendingTarget: pendingCanvasAgentAnnotationEditTargetRef.current,
isEligibleNode: (node) =>
node.type === "image" &&
isValidChatHttpUrl(String((node.data as any)?.src || "").trim()),
});
if (
pendingCanvasAgentAnnotationEditTargetRef.current &&
!pendingAnnotationTargetNode
) {
pendingCanvasAgentAnnotationEditTargetRef.current = null;
}
const effectiveSelectedNodeIds = mergePendingCanvasAgentTargetNodeIds({
selectedNodeIds,
pendingTargetNodeId: pendingAnnotationTargetNode?.id,
maxItems: 20,
});
const selectedNodeIdSet = new Set(effectiveSelectedNodeIds);
const imageNodes = nodes.filter(
(node) =>
node.type === "image" &&
@ -5647,8 +5681,15 @@ export function CanvasChatPanel({
const effectiveComposerMode = activeComposerModeRef.current || activeComposerMode;
const shouldIsolateDirectModeContext = effectiveComposerMode !== "agent";
const selectedImageNodesForContext = selectedImageNodes.filter((node) =>
isValidChatHttpUrl(String((node.data as any)?.src || "").trim())
const selectedImageNodesForContext = Array.from(
new Map(
[
...selectedImageNodes.filter((node) =>
isValidChatHttpUrl(String((node.data as any)?.src || "").trim())
),
...(pendingAnnotationTargetNode ? [pendingAnnotationTargetNode] : []),
].map((node) => [node.id, node])
).values()
);
if (shouldIsolateDirectModeContext && selectedImageNodesForContext.length === 0) {

View File

@ -0,0 +1,59 @@
import type { CanvasNode } from "@/types/canvas";
export type PendingCanvasAgentTarget = {
nodeId?: string | null;
requestedAt?: number | null;
};
const DEFAULT_PENDING_TARGET_MAX_AGE_MS = 10 * 60 * 1000;
function normalizeNodeId(value: unknown) {
return typeof value === "string" ? value.trim() : "";
}
export function resolvePendingCanvasAgentTargetNode(params: {
nodes: CanvasNode[];
pendingTarget?: PendingCanvasAgentTarget | null;
now?: number;
maxAgeMs?: number;
isEligibleNode?: (node: CanvasNode) => boolean;
}) {
const nodeId = normalizeNodeId(params.pendingTarget?.nodeId);
if (!nodeId) return null;
const requestedAt = Number(params.pendingTarget?.requestedAt || 0);
const now = Number(params.now ?? Date.now());
const maxAgeMs = Math.max(
0,
Number(params.maxAgeMs ?? DEFAULT_PENDING_TARGET_MAX_AGE_MS)
);
if (
Number.isFinite(requestedAt) &&
requestedAt > 0 &&
Number.isFinite(now) &&
now - requestedAt > maxAgeMs
) {
return null;
}
const node = params.nodes.find((item) => item.id === nodeId) || null;
if (!node) return null;
if (params.isEligibleNode && !params.isEligibleNode(node)) return null;
return node;
}
export function mergePendingCanvasAgentTargetNodeIds(params: {
selectedNodeIds: string[];
pendingTargetNodeId?: string | null;
maxItems?: number;
}) {
const maxItems = Math.max(1, Number(params.maxItems || 20));
return Array.from(
new Set(
[
...params.selectedNodeIds,
normalizeNodeId(params.pendingTargetNodeId),
].filter(Boolean)
)
).slice(0, maxItems);
}

View File

@ -12,6 +12,10 @@ import {
shouldRetryCanvasViewOperationAfterRefresh,
shouldSkipCanvasMediaPayloadForHandledItems,
} from "@/components/canvas/chat-panel/canvas-agent-stream";
import {
mergePendingCanvasAgentTargetNodeIds,
resolvePendingCanvasAgentTargetNode,
} from "@/components/canvas/chat-panel/canvas-agent-targets";
import {
writeCanvasMediaChunk,
writeCanvasOperationChunk,
@ -2237,6 +2241,94 @@ test.describe("canvas chat pure helpers", () => {
]);
});
test("keeps pending annotation edit targets eligible for the next canvas context", () => {
const nodes = [
{
id: "image-annotated",
type: "image",
position: { x: 10, y: 20 },
size: { width: 300, height: 240 },
data: {
src: "https://cdn.example.com/annotated.png",
metadata: {
annotations: [
{
id: "annotation-1",
type: "text-note",
text: "背景换浅灰",
},
],
},
},
},
{
id: "text-1",
type: "text",
position: { x: 500, y: 20 },
size: { width: 220, height: 80 },
data: { content: "说明" },
},
] as any[];
const pendingNode = resolvePendingCanvasAgentTargetNode({
nodes,
pendingTarget: {
nodeId: "image-annotated",
requestedAt: 1000,
},
now: 1500,
isEligibleNode: (node) => node.type === "image",
});
expect(pendingNode?.id).toBe("image-annotated");
expect(
resolvePendingCanvasAgentTargetNode({
nodes,
pendingTarget: {
nodeId: "image-annotated",
requestedAt: 1000,
},
now: 1000 + 10 * 60 * 1000 + 1,
isEligibleNode: (node) => node.type === "image",
})
).toBeNull();
expect(
resolvePendingCanvasAgentTargetNode({
nodes,
pendingTarget: {
nodeId: "text-1",
requestedAt: 1000,
},
now: 1500,
isEligibleNode: (node) => node.type === "image",
})
).toBeNull();
});
test("merges pending annotation edit targets into selected node ids without duplicates", () => {
expect(
mergePendingCanvasAgentTargetNodeIds({
selectedNodeIds: ["image-1", "image-2"],
pendingTargetNodeId: "image-1",
})
).toEqual(["image-1", "image-2"]);
expect(
mergePendingCanvasAgentTargetNodeIds({
selectedNodeIds: ["image-1"],
pendingTargetNodeId: "image-annotated",
})
).toEqual(["image-1", "image-annotated"]);
expect(
mergePendingCanvasAgentTargetNodeIds({
selectedNodeIds: Array.from({ length: 20 }, (_, index) => `node-${index + 1}`),
pendingTargetNodeId: "node-21",
maxItems: 20,
})
).toHaveLength(20);
});
test("builds annotation-aware edit prompts only for annotation requests", () => {
const context: CanvasAgentContext = {
...plannerContext,