diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 101bd590e..c255a8afa 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -110,6 +110,7 @@ import { shouldDeferCanvasAgentViewOperation, } from "@/lib/canvas/agent-view-interaction-guard"; import { getCanvasAgentOperationErrorMessage } from "@/lib/canvas/agent-operation-errors"; +import { shouldBypassCanvasImagePointsPrecheckForAgentControl } from "@/lib/canvas/agent-client-intent"; import { getCanvasSkillIconComponent } from "@/components/canvas/canvas-skill-icons"; import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar"; import { VideoParamsBar, type VideoGenerationParams } from "@/components/video-params-bar"; @@ -8301,6 +8302,19 @@ export function CanvasChatPanel({ const ensureImagePointsBeforeSend = useCallback(() => { if (isVideoModel || activeComposerMode === "agent") return true; + if ( + shouldBypassCanvasImagePointsPrecheckForAgentControl({ + text: input, + activeComposerMode, + attachmentCount: attachments.length, + hasSkillTag: Boolean(selectedSkillTag), + hasWorkflowContext, + isDetailGenerationMode, + agentEnabled: effectiveAgentEnabled, + }) + ) { + return true; + } if (membershipSummary?.isHighVip) return true; const pointsEnabled = membershipSummary?.pointsSystemConfig?.enabled === true; @@ -8339,20 +8353,39 @@ export function CanvasChatPanel({ return false; }, [ activeComposerMode, + attachments.length, currentModel?.imageGenConfig, + effectiveAgentEnabled, + hasWorkflowContext, imageParams?.imageCount, imageParams?.imageSize, + input, + isDetailGenerationMode, isVideoModel, membershipSummary?.isHighVip, membershipSummary?.pointsSystemConfig?.blockOnInsufficient, membershipSummary?.pointsSystemConfig?.enabled, membershipSummary?.summary?.currentCredits, + selectedSkillTag, ]); const estimatedPointsCost = useMemo(() => { if (membershipSummary?.isHighVip) return null; if (membershipSummary?.pointsSystemConfig?.enabled !== true) return null; if (activeComposerMode === "agent") return null; + if ( + shouldBypassCanvasImagePointsPrecheckForAgentControl({ + text: input, + activeComposerMode, + attachmentCount: attachments.length, + hasSkillTag: Boolean(selectedSkillTag), + hasWorkflowContext, + isDetailGenerationMode, + agentEnabled: effectiveAgentEnabled, + }) + ) { + return null; + } if (isVideoModel) { const cost = getGenerationCreditCostFromImageGenConfig( @@ -8388,12 +8421,18 @@ export function CanvasChatPanel({ : null; }, [ activeComposerMode, + attachments.length, currentModel?.imageGenConfig, + effectiveAgentEnabled, + hasWorkflowContext, imageParams?.imageCount, imageParams?.imageSize, + input, + isDetailGenerationMode, isVideoModel, membershipSummary?.isHighVip, membershipSummary?.pointsSystemConfig?.enabled, + selectedSkillTag, videoParams?.duration, videoParams?.resolution, ]); diff --git a/lib/canvas/agent-client-intent.ts b/lib/canvas/agent-client-intent.ts new file mode 100644 index 000000000..c0f330650 --- /dev/null +++ b/lib/canvas/agent-client-intent.ts @@ -0,0 +1,48 @@ +export function normalizeCanvasAgentClientIntentText(value: unknown) { + return String(value || "") + .replace(/\u200b/g, " ") + .replace(/\s+/g, " ") + .trim() + .toLowerCase(); +} + +const CANVAS_AGENT_IMAGE_GENERATION_OR_EDIT_REGEX = + /(生成|生图|出图|画一张|来一张|做一张|做图|做视频|生成视频|视频生成|换背景|背景换|白底|去背景|删除背景|抠图|高清|增强|超清|超分|修图|重绘|扩图|扩展画幅|局部重绘|风格化|海报|主图|logo)/i; + +const CANVAS_AGENT_VIEW_CONTROL_REGEX = + /(看全局|看全部|缩小看|适配全部|适配画布|聚焦选中|聚焦结果|聚焦刚生成|focus selection|focus latest|fit all|zoom in|zoom out|pan to)|(画布|视角|镜头|viewport|canvas).{0,12}(放大|缩小|缩放|聚焦|定位|居中|看全局|看全部|移动|平移|拖动|zoom|fit|focus|pan)|(放大|缩小|缩放|聚焦|定位|居中|zoom|fit|focus).{0,12}(画布|视角|镜头|全局|全部|选中|选择|刚生成|结果|节点|对象|图片|图)/i; + +const CANVAS_AGENT_SLOT_MANAGEMENT_REGEX = + /(新建|创建|添加|插入|放置|加一个|建一个).{0,10}(图片|视频|ui|画面|空)?(槽位|slot|空框|占位框|框)|(槽位|slot|空框|占位框).{0,10}(新建|创建|添加|插入|放置)/i; + +const CANVAS_AGENT_STRUCTURAL_OPERATION_REGEX = + /(删除|移除|移动|挪动|排列|对齐|重命名|改名|连线|连接|添加批注|批注|备注|标注|添加文字|文字节点|更新文字|修改文字).{0,14}(画布|节点|对象|元素|图片|图|文本|便签|选中|这些|它们|这个)|(画布|节点|对象|元素|图片|图|文本|便签|选中|这些|它们|这个).{0,14}(删除|移除|移动|挪动|排列|对齐|重命名|改名|连线|连接|添加批注|批注|备注|标注|添加文字|文字节点|更新文字|修改文字)/i; + +export function hasCanvasAgentNonGenerativeControlIntent(value: unknown) { + const text = normalizeCanvasAgentClientIntentText(value); + if (!text) return false; + if (CANVAS_AGENT_IMAGE_GENERATION_OR_EDIT_REGEX.test(text)) return false; + return ( + CANVAS_AGENT_VIEW_CONTROL_REGEX.test(text) || + CANVAS_AGENT_SLOT_MANAGEMENT_REGEX.test(text) || + CANVAS_AGENT_STRUCTURAL_OPERATION_REGEX.test(text) + ); +} + +export function shouldBypassCanvasImagePointsPrecheckForAgentControl(params: { + text: unknown; + activeComposerMode?: string | null; + attachmentCount?: number; + hasSkillTag?: boolean; + hasWorkflowContext?: boolean; + isDetailGenerationMode?: boolean; + agentEnabled?: boolean; +}) { + if (params.activeComposerMode === "agent") return true; + if (params.agentEnabled === false) return false; + if ((params.attachmentCount || 0) > 0) return false; + if (params.hasSkillTag || params.hasWorkflowContext || params.isDetailGenerationMode) { + return false; + } + return hasCanvasAgentNonGenerativeControlIntent(params.text); +} diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index 195a77702..3fdfd7f51 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -114,6 +114,10 @@ import { buildCanvasSaveRequestBody, mergeCanvasSaveRevisionOptions, } from "@/lib/canvas/canvas-save-request"; +import { + hasCanvasAgentNonGenerativeControlIntent, + shouldBypassCanvasImagePointsPrecheckForAgentControl, +} from "@/lib/canvas/agent-client-intent"; import { applyCanvasOperationPlanToCanvasData, CanvasAgentMutationError, @@ -227,6 +231,61 @@ test.describe("canvas chat pure helpers", () => { expect(isCanvasAgentViewOperationClientEnabled("1")).toBe(true); }); + test("bypasses image point precheck only for non-generative canvas agent controls", () => { + expect(hasCanvasAgentNonGenerativeControlIntent("把画布放大一点")).toBe(true); + expect(hasCanvasAgentNonGenerativeControlIntent("缩小看全局")).toBe(true); + expect(hasCanvasAgentNonGenerativeControlIntent("新建一个图片槽位")).toBe(true); + expect(hasCanvasAgentNonGenerativeControlIntent("删除选中的节点")).toBe(true); + expect(hasCanvasAgentNonGenerativeControlIntent("把选中图片背景换白")).toBe(false); + expect(hasCanvasAgentNonGenerativeControlIntent("生成一张产品主图")).toBe(false); + expect(hasCanvasAgentNonGenerativeControlIntent("抠图去背景")).toBe(false); + + expect( + shouldBypassCanvasImagePointsPrecheckForAgentControl({ + text: "把画布放大一点", + activeComposerMode: "image", + agentEnabled: true, + }) + ).toBe(true); + expect( + shouldBypassCanvasImagePointsPrecheckForAgentControl({ + text: "把画布放大一点", + activeComposerMode: "image", + attachmentCount: 1, + agentEnabled: true, + }) + ).toBe(false); + expect( + shouldBypassCanvasImagePointsPrecheckForAgentControl({ + text: "把画布放大一点", + activeComposerMode: "image", + hasWorkflowContext: true, + agentEnabled: true, + }) + ).toBe(false); + expect( + shouldBypassCanvasImagePointsPrecheckForAgentControl({ + text: "把画布放大一点", + activeComposerMode: "image", + agentEnabled: false, + }) + ).toBe(false); + expect( + shouldBypassCanvasImagePointsPrecheckForAgentControl({ + text: "生成一张产品主图", + activeComposerMode: "image", + agentEnabled: true, + }) + ).toBe(false); + expect( + shouldBypassCanvasImagePointsPrecheckForAgentControl({ + text: "任意 agent 模式文本", + activeComposerMode: "agent", + agentEnabled: true, + }) + ).toBe(true); + }); + test("rejects stale autosave bases without blocking fresh or missing revisions", () => { const currentUpdatedAt = new Date("2026-06-22T00:00:05.500Z");