fix: centralize canvas agent pending requests

This commit is contained in:
134 2026-06-23 17:52:00 +08:00
parent 914e09af73
commit b7ff4e68eb
5 changed files with 208 additions and 71 deletions

View File

@ -136,6 +136,17 @@ import {
mergePendingCanvasAgentTargetNodeIds, mergePendingCanvasAgentTargetNodeIds,
resolvePendingCanvasAgentTargetNode, resolvePendingCanvasAgentTargetNode,
} from "@/components/canvas/chat-panel/canvas-agent-targets"; } from "@/components/canvas/chat-panel/canvas-agent-targets";
import {
CANVAS_AGENT_ANNOTATION_EDIT_EVENT,
CANVAS_AGENT_SLOT_FILL_EVENT,
consumeCanvasAgentAnnotationEditRequest,
consumeCanvasAgentSlotFillRequest,
getPendingCanvasAgentAnnotationEditRequest,
getPendingCanvasAgentSlotFillRequest,
type CanvasAgentPendingRequestWindow,
type PendingCanvasAgentAnnotationEditRequest,
type PendingCanvasAgentSlotFillRequest,
} from "@/lib/canvas/agent-pending-requests";
import { import {
type DetailArchive, type DetailArchive,
type DetailBriefForm, type DetailBriefForm,
@ -3594,19 +3605,7 @@ export function CanvasChatPanel({
useEffect(() => { useEffect(() => {
if (!effectiveAgentEnabled || typeof window === "undefined") return; if (!effectiveAgentEnabled || typeof window === "undefined") return;
type PendingCanvasAgentSlotFillRequest = { const targetWindow = window as CanvasAgentPendingRequestWindow;
nodeId?: string;
label?: string;
};
type PendingCanvasAgentAnnotationEditRequest = {
nodeId?: string;
title?: string;
annotationCount?: number;
};
const targetWindow = window as Window & {
__canvasAgentPendingSlotFillRequest?: PendingCanvasAgentSlotFillRequest;
__canvasAgentPendingAnnotationEditRequest?: PendingCanvasAgentAnnotationEditRequest;
};
const focusComposerWithPrompt = (prompt: string) => { const focusComposerWithPrompt = (prompt: string) => {
handleSelectGenerationMode("agent"); handleSelectGenerationMode("agent");
@ -3630,11 +3629,8 @@ export function CanvasChatPanel({
}; };
const handleAgentSlotFillRequest = (event: Event) => { const handleAgentSlotFillRequest = (event: Event) => {
const detail = (event as CustomEvent<{ nodeId?: string; label?: string }>).detail; const detail = (event as CustomEvent<PendingCanvasAgentSlotFillRequest>).detail;
const pending = targetWindow.__canvasAgentPendingSlotFillRequest; consumeCanvasAgentSlotFillRequest(targetWindow, detail);
if (!pending?.nodeId || pending.nodeId === detail?.nodeId) {
delete targetWindow.__canvasAgentPendingSlotFillRequest;
}
const label = String(detail?.label || "这个槽位").trim(); const label = String(detail?.label || "这个槽位").trim();
const prompt = `在${label === "这个槽位" ? label : `「${label}」`}生成一张图`; const prompt = `在${label === "这个槽位" ? label : `「${label}」`}生成一张图`;
@ -3644,17 +3640,9 @@ export function CanvasChatPanel({
}; };
const handleAgentAnnotationEditRequest = (event: Event) => { const handleAgentAnnotationEditRequest = (event: Event) => {
const detail = ( const detail = (event as CustomEvent<PendingCanvasAgentAnnotationEditRequest>)
event as CustomEvent<{ .detail;
nodeId?: string; consumeCanvasAgentAnnotationEditRequest(targetWindow, detail);
title?: string;
annotationCount?: number;
}>
).detail;
const pending = targetWindow.__canvasAgentPendingAnnotationEditRequest;
if (!pending?.nodeId || pending.nodeId === detail?.nodeId) {
delete targetWindow.__canvasAgentPendingAnnotationEditRequest;
}
const nodeId = String(detail?.nodeId || "").trim(); const nodeId = String(detail?.nodeId || "").trim();
if (nodeId) { if (nodeId) {
pendingCanvasAgentAnnotationEditTargetRef.current = { pendingCanvasAgentAnnotationEditTargetRef.current = {
@ -3674,37 +3662,37 @@ export function CanvasChatPanel({
}; };
window.addEventListener( window.addEventListener(
"canvas-agent-slot-fill-request", CANVAS_AGENT_SLOT_FILL_EVENT,
handleAgentSlotFillRequest handleAgentSlotFillRequest
); );
window.addEventListener( window.addEventListener(
"canvas-agent-annotation-edit-request", CANVAS_AGENT_ANNOTATION_EDIT_EVENT,
handleAgentAnnotationEditRequest handleAgentAnnotationEditRequest
); );
const pendingSlotFillRequest = targetWindow.__canvasAgentPendingSlotFillRequest; const pendingSlotFillRequest = getPendingCanvasAgentSlotFillRequest(targetWindow);
if (pendingSlotFillRequest) { if (pendingSlotFillRequest) {
handleAgentSlotFillRequest( handleAgentSlotFillRequest(
new CustomEvent("canvas-agent-slot-fill-request", { new CustomEvent(CANVAS_AGENT_SLOT_FILL_EVENT, {
detail: pendingSlotFillRequest, detail: pendingSlotFillRequest,
}) })
); );
} }
const pendingAnnotationEditRequest = const pendingAnnotationEditRequest =
targetWindow.__canvasAgentPendingAnnotationEditRequest; getPendingCanvasAgentAnnotationEditRequest(targetWindow);
if (pendingAnnotationEditRequest) { if (pendingAnnotationEditRequest) {
handleAgentAnnotationEditRequest( handleAgentAnnotationEditRequest(
new CustomEvent("canvas-agent-annotation-edit-request", { new CustomEvent(CANVAS_AGENT_ANNOTATION_EDIT_EVENT, {
detail: pendingAnnotationEditRequest, detail: pendingAnnotationEditRequest,
}) })
); );
} }
return () => { return () => {
window.removeEventListener( window.removeEventListener(
"canvas-agent-slot-fill-request", CANVAS_AGENT_SLOT_FILL_EVENT,
handleAgentSlotFillRequest handleAgentSlotFillRequest
); );
window.removeEventListener( window.removeEventListener(
"canvas-agent-annotation-edit-request", CANVAS_AGENT_ANNOTATION_EDIT_EVENT,
handleAgentAnnotationEditRequest handleAgentAnnotationEditRequest
); );
}; };

View File

@ -60,6 +60,12 @@ import {
LOCAL_UPLOAD_PENDING_METADATA_KEY, LOCAL_UPLOAD_PENDING_METADATA_KEY,
LOCAL_UPLOAD_PREVIEW_METADATA_KEY, LOCAL_UPLOAD_PREVIEW_METADATA_KEY,
} from "@/lib/canvas/local-upload-state"; } from "@/lib/canvas/local-upload-state";
import {
dispatchCanvasAgentOpenChatPanelRequest,
dispatchCanvasAgentSlotFillRequest,
storeCanvasAgentSlotFillRequest,
type CanvasAgentPendingRequestWindow,
} from "@/lib/canvas/agent-pending-requests";
import { inferOriginalImageUrl } from "@/lib/image-url"; import { inferOriginalImageUrl } from "@/lib/image-url";
const CANVAS_NO_DRAG_SELECTOR = const CANVAS_NO_DRAG_SELECTOR =
@ -691,24 +697,14 @@ export function CanvasWorkspace({
nodeId: agentSlotContextMenu.nodeId, nodeId: agentSlotContextMenu.nodeId,
label: agentSlotContextMenu.label, label: agentSlotContextMenu.label,
}; };
const targetWindow = window as Window & { const targetWindow = window as CanvasAgentPendingRequestWindow;
__canvasAgentPendingSlotFillRequest?: typeof detail;
};
selectNode(agentSlotContextMenu.nodeId, false); selectNode(agentSlotContextMenu.nodeId, false);
targetWindow.__canvasAgentPendingSlotFillRequest = detail; storeCanvasAgentSlotFillRequest(targetWindow, detail);
targetWindow.dispatchEvent( dispatchCanvasAgentOpenChatPanelRequest(targetWindow, {
new CustomEvent("canvas-open-chat-panel-request", { reason: "agent-slot-fill",
detail: { nodeId: detail.nodeId,
reason: "agent-slot-fill", });
nodeId: detail.nodeId, dispatchCanvasAgentSlotFillRequest(targetWindow, detail);
},
})
);
targetWindow.dispatchEvent(
new CustomEvent("canvas-agent-slot-fill-request", {
detail,
})
);
setAgentSlotContextMenu(null); setAgentSlotContextMenu(null);
}, [agentSlotContextMenu, selectNode]); }, [agentSlotContextMenu, selectNode]);

View File

@ -125,6 +125,12 @@ import {
syncCanvasLocalEditRegionAnnotationsMetadata, syncCanvasLocalEditRegionAnnotationsMetadata,
syncCanvasNodeNoteAnnotationMetadata, syncCanvasNodeNoteAnnotationMetadata,
} from "@/lib/canvas/agent-annotation-metadata"; } from "@/lib/canvas/agent-annotation-metadata";
import {
dispatchCanvasAgentAnnotationEditRequest,
dispatchCanvasAgentOpenChatPanelRequest,
storeCanvasAgentAnnotationEditRequest,
type CanvasAgentPendingRequestWindow,
} from "@/lib/canvas/agent-pending-requests";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@ -6685,25 +6691,15 @@ export function ImageNode({
title: displayTitle, title: displayTitle,
annotationCount: canvasAgentAnnotations.length, annotationCount: canvasAgentAnnotations.length,
}; };
const targetWindow = window as Window & { const targetWindow = window as CanvasAgentPendingRequestWindow;
__canvasAgentPendingAnnotationEditRequest?: typeof detail;
};
selectNode(node.id, false); selectNode(node.id, false);
targetWindow.__canvasAgentPendingAnnotationEditRequest = detail; storeCanvasAgentAnnotationEditRequest(targetWindow, detail);
targetWindow.dispatchEvent( dispatchCanvasAgentOpenChatPanelRequest(targetWindow, {
new CustomEvent("canvas-open-chat-panel-request", { reason: "agent-annotation-edit",
detail: { nodeId: detail.nodeId,
reason: "agent-annotation-edit", });
nodeId: detail.nodeId, dispatchCanvasAgentAnnotationEditRequest(targetWindow, detail);
},
})
);
targetWindow.dispatchEvent(
new CustomEvent("canvas-agent-annotation-edit-request", {
detail,
})
);
}, [ }, [
canvasAgentAnnotations.length, canvasAgentAnnotations.length,
displayTitle, displayTitle,

View File

@ -0,0 +1,103 @@
export type PendingCanvasAgentSlotFillRequest = {
nodeId?: string;
label?: string;
};
export type PendingCanvasAgentAnnotationEditRequest = {
nodeId?: string;
title?: string;
annotationCount?: number;
};
export type CanvasAgentPendingRequestWindow = Window & {
__canvasAgentPendingSlotFillRequest?: PendingCanvasAgentSlotFillRequest;
__canvasAgentPendingAnnotationEditRequest?: PendingCanvasAgentAnnotationEditRequest;
};
export const CANVAS_AGENT_OPEN_CHAT_PANEL_EVENT = "canvas-open-chat-panel-request";
export const CANVAS_AGENT_SLOT_FILL_EVENT = "canvas-agent-slot-fill-request";
export const CANVAS_AGENT_ANNOTATION_EDIT_EVENT =
"canvas-agent-annotation-edit-request";
function normalizeNodeId(value: unknown) {
return String(value || "").trim();
}
export function storeCanvasAgentSlotFillRequest(
targetWindow: CanvasAgentPendingRequestWindow,
request: PendingCanvasAgentSlotFillRequest
) {
targetWindow.__canvasAgentPendingSlotFillRequest = { ...request };
}
export function getPendingCanvasAgentSlotFillRequest(
targetWindow: CanvasAgentPendingRequestWindow
) {
return targetWindow.__canvasAgentPendingSlotFillRequest || null;
}
export function consumeCanvasAgentSlotFillRequest(
targetWindow: CanvasAgentPendingRequestWindow,
detail?: PendingCanvasAgentSlotFillRequest | null
) {
const pending = getPendingCanvasAgentSlotFillRequest(targetWindow);
if (!pending) return null;
const pendingNodeId = normalizeNodeId(pending.nodeId);
const detailNodeId = normalizeNodeId(detail?.nodeId);
if (!pendingNodeId || pendingNodeId === detailNodeId) {
delete targetWindow.__canvasAgentPendingSlotFillRequest;
}
return pending;
}
export function storeCanvasAgentAnnotationEditRequest(
targetWindow: CanvasAgentPendingRequestWindow,
request: PendingCanvasAgentAnnotationEditRequest
) {
targetWindow.__canvasAgentPendingAnnotationEditRequest = { ...request };
}
export function getPendingCanvasAgentAnnotationEditRequest(
targetWindow: CanvasAgentPendingRequestWindow
) {
return targetWindow.__canvasAgentPendingAnnotationEditRequest || null;
}
export function consumeCanvasAgentAnnotationEditRequest(
targetWindow: CanvasAgentPendingRequestWindow,
detail?: PendingCanvasAgentAnnotationEditRequest | null
) {
const pending = getPendingCanvasAgentAnnotationEditRequest(targetWindow);
if (!pending) return null;
const pendingNodeId = normalizeNodeId(pending.nodeId);
const detailNodeId = normalizeNodeId(detail?.nodeId);
if (!pendingNodeId || pendingNodeId === detailNodeId) {
delete targetWindow.__canvasAgentPendingAnnotationEditRequest;
}
return pending;
}
export function dispatchCanvasAgentOpenChatPanelRequest(
targetWindow: Window,
detail: { reason: "agent-slot-fill" | "agent-annotation-edit"; nodeId?: string }
) {
targetWindow.dispatchEvent(
new CustomEvent(CANVAS_AGENT_OPEN_CHAT_PANEL_EVENT, { detail })
);
}
export function dispatchCanvasAgentSlotFillRequest(
targetWindow: Window,
detail: PendingCanvasAgentSlotFillRequest
) {
targetWindow.dispatchEvent(new CustomEvent(CANVAS_AGENT_SLOT_FILL_EVENT, { detail }));
}
export function dispatchCanvasAgentAnnotationEditRequest(
targetWindow: Window,
detail: PendingCanvasAgentAnnotationEditRequest
) {
targetWindow.dispatchEvent(
new CustomEvent(CANVAS_AGENT_ANNOTATION_EDIT_EVENT, { detail })
);
}

View File

@ -16,6 +16,15 @@ import {
mergePendingCanvasAgentTargetNodeIds, mergePendingCanvasAgentTargetNodeIds,
resolvePendingCanvasAgentTargetNode, resolvePendingCanvasAgentTargetNode,
} from "@/components/canvas/chat-panel/canvas-agent-targets"; } from "@/components/canvas/chat-panel/canvas-agent-targets";
import {
consumeCanvasAgentAnnotationEditRequest,
consumeCanvasAgentSlotFillRequest,
getPendingCanvasAgentAnnotationEditRequest,
getPendingCanvasAgentSlotFillRequest,
storeCanvasAgentAnnotationEditRequest,
storeCanvasAgentSlotFillRequest,
type CanvasAgentPendingRequestWindow,
} from "@/lib/canvas/agent-pending-requests";
import { import {
writeCanvasMediaChunk, writeCanvasMediaChunk,
writeCanvasOperationChunk, writeCanvasOperationChunk,
@ -2675,6 +2684,51 @@ test.describe("canvas chat pure helpers", () => {
]); ]);
}); });
test("keeps pending canvas agent UI requests until the matching panel event consumes them", () => {
const targetWindow = {} as CanvasAgentPendingRequestWindow;
const slotRequest = {
nodeId: "slot-1",
label: "主图槽",
};
const annotationRequest = {
nodeId: "image-annotated",
title: "白底主图",
annotationCount: 2,
};
storeCanvasAgentSlotFillRequest(targetWindow, slotRequest);
storeCanvasAgentAnnotationEditRequest(targetWindow, annotationRequest);
expect(getPendingCanvasAgentSlotFillRequest(targetWindow)).toEqual(slotRequest);
expect(getPendingCanvasAgentAnnotationEditRequest(targetWindow)).toEqual(
annotationRequest
);
expect(
consumeCanvasAgentSlotFillRequest(targetWindow, { nodeId: "other-slot" })
).toEqual(slotRequest);
expect(
consumeCanvasAgentAnnotationEditRequest(targetWindow, {
nodeId: "other-image",
})
).toEqual(annotationRequest);
expect(getPendingCanvasAgentSlotFillRequest(targetWindow)).toEqual(slotRequest);
expect(getPendingCanvasAgentAnnotationEditRequest(targetWindow)).toEqual(
annotationRequest
);
expect(
consumeCanvasAgentSlotFillRequest(targetWindow, { nodeId: "slot-1" })
).toEqual(slotRequest);
expect(
consumeCanvasAgentAnnotationEditRequest(targetWindow, {
nodeId: "image-annotated",
})
).toEqual(annotationRequest);
expect(getPendingCanvasAgentSlotFillRequest(targetWindow)).toBeNull();
expect(getPendingCanvasAgentAnnotationEditRequest(targetWindow)).toBeNull();
});
test("keeps pending annotation edit targets eligible for the next canvas context", () => { test("keeps pending annotation edit targets eligible for the next canvas context", () => {
const nodes = [ const nodes = [
{ {