fix: bypass clarification for selected canvas slots

This commit is contained in:
134 2026-06-22 22:14:14 +08:00
parent 7d99330eb9
commit bcea56c0d9
4 changed files with 179 additions and 1 deletions

View File

@ -164,6 +164,7 @@ import {
resolveClarificationBypass,
resolveClarificationMessage,
} from "@/lib/ai/runtime/clarification-interruption";
import { shouldBypassCanvasAgentMissingTargetClarification } from "@/lib/ai/runtime/canvas-agent-clarification";
import {
buildAgentToolImagesMarkdown,
buildAgentToolVideoMarkdown,
@ -4625,6 +4626,27 @@ export async function POST(request: Request) {
return createSseResponse(promptOnlyStream);
}
if (shouldRunAgentConversation && smartAutoNeedsClarification) {
const canvasAgentSlotClarificationBypass =
shouldBypassCanvasAgentMissingTargetClarification({
text: requestTextContent,
contextPayload: effectiveContextPayload,
executionMode: smartAutoExecution?.mode,
});
if (canvasAgentSlotClarificationBypass.shouldBypass) {
smartAutoNeedsClarification = false;
smartAutoClarificationQuestion = undefined;
smartAutoFriendlyMessage = "收到,我会把结果填入当前选中的画布槽位。";
chatApiLogger.info(
"[Smart Auto Mode] ✅ Skip clarification for selected canvas Agent slot",
{
reason: canvasAgentSlotClarificationBypass.reason,
executionMode: smartAutoExecution?.mode,
}
);
}
}
if (shouldRunAgentConversation && smartAutoNeedsClarification) {
const clarificationBypass = resolveClarificationBypass({
text: message.parts

View File

@ -0,0 +1,67 @@
import type { ContextPayload } from "@/lib/ai/context-memory/types";
type CanvasAgentExecutionMode = "edit_image" | "generate_image" | "generate_video";
const CANVAS_AGENT_SLOT_GENERATION_REGEX =
/(生成|生图|出图|画一张|来一张|做一张|做图|做视频|视频生成|生成视频|填充|放到|放进|在这里|这个空框|这个槽位|槽位|slot)/i;
function isCanvasSource(contextPayload?: ContextPayload | null) {
return (
contextPayload?.source === "canvas" || contextPayload?.source === "realtime-canvas"
);
}
function hasSelectedCanvasAgentSlot(contextPayload?: ContextPayload | null) {
const context = contextPayload?.canvasAgentContext;
if (!context) return false;
const selectedNodeIds = new Set(
(context.selectedNodeIds || contextPayload?.selectedNodeIds || [])
.map((id) => String(id || "").trim())
.filter(Boolean)
);
return (context.slots || []).some((slot) => {
const slotId = String(slot.nodeId || "").trim();
return Boolean(slotId && (slot.isSelected || selectedNodeIds.has(slotId)));
});
}
export function shouldBypassCanvasAgentMissingTargetClarification(params: {
text: string;
contextPayload?: ContextPayload | null;
executionMode?: string | null;
}) {
const executionMode = String(params.executionMode || "") as CanvasAgentExecutionMode;
if (executionMode !== "generate_image" && executionMode !== "generate_video") {
return {
shouldBypass: false,
reason: undefined,
};
}
if (!isCanvasSource(params.contextPayload)) {
return {
shouldBypass: false,
reason: undefined,
};
}
if (!hasSelectedCanvasAgentSlot(params.contextPayload)) {
return {
shouldBypass: false,
reason: undefined,
};
}
if (!CANVAS_AGENT_SLOT_GENERATION_REGEX.test(String(params.text || ""))) {
return {
shouldBypass: false,
reason: undefined,
};
}
return {
shouldBypass: true,
reason: "selected_canvas_agent_slot_generation_intent",
};
}

View File

@ -9,6 +9,7 @@ import type {
} from "@/lib/ai/context-memory/types";
import type { IntentArbiterMode } from "@/lib/ai/runtime/intent-arbiter";
import type { TaskBoundaryDecision } from "@/lib/ai/runtime/task-boundary-classifier";
import { shouldBypassCanvasAgentMissingTargetClarification } from "@/lib/ai/runtime/canvas-agent-clarification";
export interface CanvasAgentClarificationGateDecision {
shouldClarify: boolean;
@ -205,6 +206,12 @@ export function resolveCanvasAgentClarificationGate(params: {
const targetResolution = params.targetResolution;
const selectedRefs = mediaInput?.selectedImageRefs || [];
const candidateRefs = targetResolution?.candidateRefs || mediaInput?.imageRefs || [];
const missingTargetClarificationBypass =
shouldBypassCanvasAgentMissingTargetClarification({
text,
contextPayload: params.contextPayload,
executionMode: params.executionMode,
});
if (
params.executionMode === "edit_image" &&
@ -219,7 +226,10 @@ export function resolveCanvasAgentClarificationGate(params: {
} satisfies CanvasAgentClarificationGateDecision;
}
if (targetResolution?.needsClarification) {
if (
targetResolution?.needsClarification &&
!missingTargetClarificationBypass.shouldBypass
) {
return {
shouldClarify: true,
clarificationMessage: buildMissingTargetClarificationMessage({

View File

@ -20,6 +20,7 @@ import {
buildCanvasAgentAnnotationEditPrompt,
hasCanvasAgentAnnotationEditIntent,
} from "@/lib/canvas/agent-annotation-prompt";
import { shouldBypassCanvasAgentMissingTargetClarification } from "@/lib/ai/runtime/canvas-agent-clarification";
import {
CANVAS_LOCAL_EDIT_REGION_ANNOTATION_SOURCE,
CANVAS_NODE_NOTE_ANNOTATION_SOURCE,
@ -1545,6 +1546,84 @@ test.describe("canvas chat pure helpers", () => {
expect(context?.focusPolicy).toBe("slot");
});
test("bypasses missing-target clarification for selected agent slot generation", () => {
const slotOnlyContext = {
source: "canvas" as const,
projectId: "11111111-1111-4111-8111-111111111111",
selectedNodeIds: ["slot-1"],
canvasImageNodes: [],
canvasAgentContext: {
version: 1 as const,
source: "canvas" as const,
projectId: "11111111-1111-4111-8111-111111111111",
selectedNodeIds: ["slot-1"],
selectedNodes: [
{
nodeId: "slot-1",
type: "agent-slot" as const,
x: 120,
y: 80,
width: 320,
height: 240,
isSelected: true,
},
],
candidateNodes: [],
slots: [
{
nodeId: "slot-1",
slotType: "image" as const,
x: 120,
y: 80,
width: 320,
height: 240,
aspectRatio: "4:3",
isSelected: true,
fillPolicy: "replace-empty" as const,
},
],
annotations: [],
viewport: { x: 0, y: 0, zoom: 1 },
focusPolicy: "slot" as const,
},
};
expect(
shouldBypassCanvasAgentMissingTargetClarification({
text: "在这里生成一张电商主图",
contextPayload: slotOnlyContext,
executionMode: "generate_image",
})
).toMatchObject({
shouldBypass: true,
reason: "selected_canvas_agent_slot_generation_intent",
});
expect(
shouldBypassCanvasAgentMissingTargetClarification({
text: "把这张图背景换白",
contextPayload: slotOnlyContext,
executionMode: "edit_image",
}).shouldBypass
).toBe(false);
expect(
shouldBypassCanvasAgentMissingTargetClarification({
text: "在这里生成一张电商主图",
contextPayload: {
...slotOnlyContext,
selectedNodeIds: [],
canvasAgentContext: {
...slotOnlyContext.canvasAgentContext,
selectedNodeIds: [],
slots: [{ ...slotOnlyContext.canvasAgentContext.slots[0], isSelected: false }],
},
},
executionMode: "generate_image",
}).shouldBypass
).toBe(false);
});
test("keeps view-only canvas agent context without nodes or media", () => {
const context = normalizeCanvasAgentContext({
source: "canvas",