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

View File

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

View File

@ -125,6 +125,12 @@ import {
syncCanvasLocalEditRegionAnnotationsMetadata,
syncCanvasNodeNoteAnnotationMetadata,
} 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 {
Dialog,
@ -6685,25 +6691,15 @@ export function ImageNode({
title: displayTitle,
annotationCount: canvasAgentAnnotations.length,
};
const targetWindow = window as Window & {
__canvasAgentPendingAnnotationEditRequest?: typeof detail;
};
const targetWindow = window as CanvasAgentPendingRequestWindow;
selectNode(node.id, false);
targetWindow.__canvasAgentPendingAnnotationEditRequest = detail;
targetWindow.dispatchEvent(
new CustomEvent("canvas-open-chat-panel-request", {
detail: {
reason: "agent-annotation-edit",
nodeId: detail.nodeId,
},
})
);
targetWindow.dispatchEvent(
new CustomEvent("canvas-agent-annotation-edit-request", {
detail,
})
);
storeCanvasAgentAnnotationEditRequest(targetWindow, detail);
dispatchCanvasAgentOpenChatPanelRequest(targetWindow, {
reason: "agent-annotation-edit",
nodeId: detail.nodeId,
});
dispatchCanvasAgentAnnotationEditRequest(targetWindow, detail);
}, [
canvasAgentAnnotations.length,
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,
resolvePendingCanvasAgentTargetNode,
} from "@/components/canvas/chat-panel/canvas-agent-targets";
import {
consumeCanvasAgentAnnotationEditRequest,
consumeCanvasAgentSlotFillRequest,
getPendingCanvasAgentAnnotationEditRequest,
getPendingCanvasAgentSlotFillRequest,
storeCanvasAgentAnnotationEditRequest,
storeCanvasAgentSlotFillRequest,
type CanvasAgentPendingRequestWindow,
} from "@/lib/canvas/agent-pending-requests";
import {
writeCanvasMediaChunk,
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", () => {
const nodes = [
{