fix: preserve canvas context for slot controls

This commit is contained in:
134 2026-06-23 20:33:40 +08:00
parent d9bb014742
commit f23fae7676
3 changed files with 96 additions and 15 deletions

View File

@ -98,6 +98,7 @@ import { shouldBypassCanvasImagePointsPrecheckForAgentControl } from "@/lib/canv
import { applyCanvasOperationPlan } from "@/lib/canvas/agent-mutations";
import { isProductionEnvironment } from "@/lib/constants";
import type {
CanvasAgentContext,
CanvasOperationPlan,
CanvasViewOperationPlan,
} from "@/types/canvas/agent-protocol";
@ -881,23 +882,28 @@ export async function POST(request: Request) {
return "";
}
})();
const isCanvasPageRequest = (() => {
const canvasPageRequest = (() => {
const referer = request.headers.get("referer");
if (!referer) return false;
if (!referer) return { isCanvasPage: false, projectId: "" };
try {
const pathname = new URL(referer).pathname;
return pathname === "/canvas" || pathname.startsWith("/canvas/");
const match = pathname.match(/^\/canvas\/([^/?#]+)/);
return {
isCanvasPage: pathname === "/canvas" || pathname.startsWith("/canvas/"),
projectId: match?.[1] || "",
};
} catch {
return false;
return { isCanvasPage: false, projectId: "" };
}
})();
const isCanvasPageRequest = canvasPageRequest.isCanvasPage;
const requestTextContent = message.parts
.filter((part): part is { type: "text"; text: string } => part.type === "text")
.map((part) => part.text)
.join(" ")
.trim();
const autoContextText = typeof autoContext === "string" ? autoContext.trim() : "";
const requestCanvasAgentContext = normalizeCanvasAgentContext(
const initialRequestCanvasAgentContext = normalizeCanvasAgentContext(
contextPayload || undefined
);
const canvasAgentFeatures = getCanvasAgentFeatureFlags();
@ -910,7 +916,7 @@ export async function POST(request: Request) {
composerMode !== "agent" &&
(contextPayload?.source === "canvas" ||
contextPayload?.source === "realtime-canvas" ||
Boolean(requestCanvasAgentContext) ||
Boolean(initialRequestCanvasAgentContext) ||
isCanvasPageRequest) &&
shouldBypassCanvasImagePointsPrecheckForAgentControl({
text: requestTextContent,
@ -920,6 +926,35 @@ export async function POST(request: Request) {
hasWorkflowContext: Boolean(agentModePayload),
agentEnabled: true,
});
const inferredCanvasProjectId =
contextPayload?.projectId ||
initialRequestCanvasAgentContext?.projectId ||
canvasPageRequest.projectId ||
(isCanvasPageRequest ? id : "");
const requestCanvasAgentContext: CanvasAgentContext | null =
initialRequestCanvasAgentContext ||
(hasCanvasControlIntentFastPath && inferredCanvasProjectId
? {
version: 1,
source: "canvas",
projectId: inferredCanvasProjectId,
viewport: { x: 0, y: 0, zoom: 1 },
selectedNodeIds: [],
selectedNodes: [],
candidateNodes: [],
slots: [],
annotations: [],
focusPolicy: "auto",
}
: null);
const canvasAgentProjectId =
contextPayload?.projectId ||
requestCanvasAgentContext?.projectId ||
inferredCanvasProjectId ||
"";
const isCanvasAgentCanvasRequest =
contextPayload?.source === "canvas" ||
(hasCanvasControlIntentFastPath && Boolean(canvasAgentProjectId));
if (requestCanvasAgentContext) {
chatApiLogger.info("[CanvasAgentContext] Normalized request context:", {
source: requestCanvasAgentContext.source,
@ -3401,7 +3436,7 @@ export async function POST(request: Request) {
const resolveCanvasLinkOperationPlan = (): CanvasOperationPlan | null => {
if (!canApplyCanvasAgentMutation) return null;
if (composerMode !== "agent" && !hasCanvasControlIntentFastPath) return null;
if (contextPayload?.source !== "canvas" || !contextPayload.projectId) return null;
if (!isCanvasAgentCanvasRequest || !canvasAgentProjectId) return null;
if (!requestCanvasAgentContext) return null;
const text = requestTextContent.trim().toLowerCase();
@ -3428,7 +3463,7 @@ export async function POST(request: Request) {
version: 1,
operationId: `${message.id}:canvas-link`,
operation: "link_nodes",
projectId: contextPayload.projectId,
projectId: canvasAgentProjectId,
baseCanvasRevision: requestCanvasAgentContext.canvasRevision,
baseCanvasUpdatedAt: requestCanvasAgentContext.canvasUpdatedAt,
sourceNodeIds: nodeIds,
@ -3471,7 +3506,7 @@ export async function POST(request: Request) {
const resolveCanvasTextOperationPlan = (): CanvasOperationPlan | null => {
if (!canApplyCanvasAgentMutation) return null;
if (composerMode !== "agent" && !hasCanvasControlIntentFastPath) return null;
if (contextPayload?.source !== "canvas" || !contextPayload.projectId) return null;
if (!isCanvasAgentCanvasRequest || !canvasAgentProjectId) return null;
if (!requestCanvasAgentContext) return null;
const text = requestTextContent.trim();
@ -3498,7 +3533,7 @@ export async function POST(request: Request) {
version: 1,
operationId: `${message.id}:canvas-update-text`,
operation: "update_text",
projectId: contextPayload.projectId,
projectId: canvasAgentProjectId,
baseCanvasRevision: requestCanvasAgentContext.canvasRevision,
baseCanvasUpdatedAt: requestCanvasAgentContext.canvasUpdatedAt,
targetNodeIds: selectedTextNodeIds,
@ -3516,12 +3551,12 @@ export async function POST(request: Request) {
(): CanvasAgentExplicitOperationPlan | null => {
if (!canApplyCanvasAgentMutation) return null;
if (composerMode !== "agent" && !hasCanvasControlIntentFastPath) return null;
if (contextPayload?.source !== "canvas" || !contextPayload.projectId) return null;
if (!isCanvasAgentCanvasRequest || !canvasAgentProjectId) return null;
if (!requestCanvasAgentContext) return null;
return buildCanvasAgentExplicitOperationPlan({
context: requestCanvasAgentContext,
projectId: contextPayload.projectId,
projectId: canvasAgentProjectId,
messageId: message.id,
chatId: id,
userText: requestTextContent,
@ -3534,7 +3569,7 @@ export async function POST(request: Request) {
const resolveCanvasModelPlannerOutcome =
async (): Promise<CanvasAgentModelPlannerOutcome | null> => {
if (composerMode !== "agent" && !hasCanvasControlIntentFastPath) return null;
if (contextPayload?.source !== "canvas" || !contextPayload.projectId) return null;
if (!isCanvasAgentCanvasRequest || !canvasAgentProjectId) return null;
if (!requestCanvasAgentContext) return null;
if (
!canvasAgentFeatures.viewOperationEnabled &&
@ -3561,7 +3596,7 @@ export async function POST(request: Request) {
model: plannerModel || myProvider.languageModel("chat-model"),
userText: requestTextContent,
context: requestCanvasAgentContext,
projectId: contextPayload.projectId,
projectId: canvasAgentProjectId,
messageId: message.id,
chatId: id,
});

View File

@ -5890,6 +5890,16 @@ export function CanvasChatPanel({
const effectiveComposerMode = activeComposerModeRef.current || activeComposerMode;
const shouldIsolateDirectModeContext = effectiveComposerMode !== "agent";
const shouldPreserveCanvasControlContext =
shouldBypassCanvasImagePointsPrecheckForAgentControl({
text: input,
activeComposerMode: effectiveComposerMode,
attachmentCount: attachments.length,
hasSkillTag: Boolean(selectedSkillTag),
hasWorkflowContext,
isDetailGenerationMode,
agentEnabled: effectiveAgentEnabled,
});
const selectedImageNodesForContext = Array.from(
new Map(
[
@ -5901,7 +5911,11 @@ export function CanvasChatPanel({
).values()
);
if (shouldIsolateDirectModeContext && selectedImageNodesForContext.length === 0) {
if (
shouldIsolateDirectModeContext &&
selectedImageNodesForContext.length === 0 &&
!shouldPreserveCanvasControlContext
) {
generationAnchorNodesRef.current = [];
return undefined;
}
@ -6172,10 +6186,16 @@ export function CanvasChatPanel({
};
}, [
activeComposerMode,
attachments.length,
canvasId,
canvasRevision,
canvasUpdatedAt,
effectiveAgentEnabled,
hasWorkflowContext,
input,
isDetailGenerationMode,
nodes,
selectedSkillTag,
selectedImageNodes,
selectedNodeIds,
viewport,

View File

@ -235,6 +235,7 @@ test.describe("canvas chat pure helpers", () => {
expect(hasCanvasAgentNonGenerativeControlIntent("把画布放大一点")).toBe(true);
expect(hasCanvasAgentNonGenerativeControlIntent("缩小看全局")).toBe(true);
expect(hasCanvasAgentNonGenerativeControlIntent("新建一个图片槽位")).toBe(true);
expect(hasCanvasAgentNonGenerativeControlIntent("创建一个图片槽位")).toBe(true);
expect(hasCanvasAgentNonGenerativeControlIntent("删除选中的节点")).toBe(true);
expect(hasCanvasAgentNonGenerativeControlIntent("把选中图片背景换白")).toBe(false);
expect(hasCanvasAgentNonGenerativeControlIntent("生成一张产品主图")).toBe(false);
@ -1126,6 +1127,31 @@ test.describe("canvas chat pure helpers", () => {
},
});
expect(outcome?.reply).toContain("视频槽位");
const imageOutcome = buildCanvasAgentExplicitOperationPlan({
context,
projectId: plannerContext.projectId || "",
messageId: "message-2",
chatId: "chat-1",
userText: "创建一个图片槽位",
});
expect(imageOutcome?.operationPlan).toMatchObject({
operation: "create_agent_slot",
targetPosition: { x: 340, y: 240 },
slot: {
slotType: "image",
label: "图片槽位",
aspectRatio: "1:1",
fillPolicy: "replace-empty",
},
metadata: {
messageId: "message-2",
chatId: "chat-1",
planner: "deterministic",
},
});
expect(imageOutcome?.reply).toContain("图片槽位");
});
test("builds deterministic node rename plans without treating titles as image edits", () => {