From 16f23b3f7ca19341c95044a167dcd912870dbf5d Mon Sep 17 00:00:00 2001 From: 134 Date: Tue, 7 Jul 2026 21:38:24 +0800 Subject: [PATCH] fix canvas cutout intent routing --- lib/ai/runtime/canvas-agent-kernel.ts | 4 +- lib/ai/runtime/intent-arbiter.ts | 32 ++++++------- lib/ai/smart-analyzer.ts | 10 ++-- lib/ai/specialists/index.ts | 2 +- tests/routes/canvas-chat-helpers.test.ts | 60 ++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 26 deletions(-) diff --git a/lib/ai/runtime/canvas-agent-kernel.ts b/lib/ai/runtime/canvas-agent-kernel.ts index 68467384f..6c5ae36f2 100644 --- a/lib/ai/runtime/canvas-agent-kernel.ts +++ b/lib/ai/runtime/canvas-agent-kernel.ts @@ -20,7 +20,7 @@ const DEICTIC_TARGET_REGEX = /(这张图|这只|这个|这款|这套|当前这张|当前这个|当前这只|这一张|这一只|这个产品|这个物体|这只猫|这只狗)/i; const ACTIONABLE_IMAGE_EDIT_REGEX = - /(抠图|去背景|透明背景|去背|扣图|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|多视角|换视角|改视角|视角变体|新角度|换角度|改角度|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|调色|调整颜色|亮度|对比度|饱和度|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调|处理)/i; + /(抠图|去背景|透明背景|去背|扣图|抠出|扣出|抠出来|扣出来|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|多视角|换视角|改视角|视角变体|新角度|换角度|改角度|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|调色|调整颜色|亮度|对比度|饱和度|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调|处理)/i; export function normalizeCanvasAgentRequestText(text: string) { return String(text || "").toLowerCase().replace(/\s+/g, "").trim(); @@ -42,7 +42,7 @@ export function inferCanvasAgentImageEditCapability( if (/去水印|水印|去logo|移除logo|去除logo/.test(normalizedText)) { return "remove_watermark"; } - if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(normalizedText)) { + if (/抠图|去背景|透明背景|去背|扣图|抠出|扣出|抠出来|扣出来|移除背景/.test(normalizedText)) { return "remove_background"; } if (/多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位/.test(normalizedText)) { diff --git a/lib/ai/runtime/intent-arbiter.ts b/lib/ai/runtime/intent-arbiter.ts index ce1081ef1..4cb6141b0 100644 --- a/lib/ai/runtime/intent-arbiter.ts +++ b/lib/ai/runtime/intent-arbiter.ts @@ -94,10 +94,10 @@ function isExplicitImageGenerationRequest(text: string): boolean { const visualTarget = "(?:图|图片|海报|头像|插画|场景|主图|产品图|商品图|电商图|壁纸|封面|logo|图标)"; const createVisualTarget = new RegExp( - `(?:生成|创作|创建|制作|设计|做|出|画|绘制)${countToken}?张?.{0,32}${visualTarget}`, + `(?:生成|创作|创建|制作|设计|做|出|画(?!布)|绘制)${countToken}?张?.{0,32}${visualTarget}`, ); const drawSubject = new RegExp( - `(?:画|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`, + `(?:画(?!布)|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`, ); const continueVariants = new RegExp( `(?:再来|再出|再做|再给我|补|多来|继续生成|继续出)${countToken}?张`, @@ -197,6 +197,9 @@ function buildExecutionFromAnalysis(params: { targetSource: ResolvedTargetAssets["resolutionMode"]; }): IntentArbiterExecution { const mode = mapIntentToMode(params.analysis.intent, params.analysis.needsClarification); + const shouldKeepPlannedToolCalls = + params.execution?.mode === mode && + params.execution?.routeIntent === params.analysis.intent; return { mode, @@ -218,7 +221,7 @@ function buildExecutionFromAnalysis(params: { shouldStayConversational: mode === "respond" || mode === "analyze_image" || mode === "clarify", shouldPublishToCanvas: params.execution?.shouldPublishToCanvas || false, - toolCalls: params.execution?.toolCalls || [], + toolCalls: shouldKeepPlannedToolCalls ? params.execution?.toolCalls || [] : [], availableTools: params.execution?.availableTools || [], }; } @@ -262,6 +265,14 @@ function resolvePolicyIntent(params: { }; } + if (hasUsableImage && likelyEdit) { + return { + intent: "image_edit", + subIntent: params.analysis.subIntent || inferDefaultImageEditSubIntent(params.text), + reason: "image_edit_policy_override", + }; + } + switch (params.actionResolution.kind) { case "image_edit_followup": return { @@ -292,13 +303,6 @@ function resolvePolicyIntent(params: { reason: "task_boundary_execution_video", }; case "execution_image": - if (hasUsableImage && likelyEdit && !explicitImageGen) { - return { - intent: "image_edit", - subIntent: params.analysis.subIntent || inferDefaultImageEditSubIntent(params.text), - reason: "task_boundary_execution_image_edit", - }; - } return { intent: "image_gen", reason: "task_boundary_execution_image_generate", @@ -329,14 +333,6 @@ function resolvePolicyIntent(params: { }; } - if (hasUsableImage && likelyEdit && params.analysis.intent === "chat") { - return { - intent: "image_edit", - subIntent: params.analysis.subIntent || inferDefaultImageEditSubIntent(params.text), - reason: "image_edit_policy_override", - }; - } - return { intent: params.analysis.intent, subIntent: diff --git a/lib/ai/smart-analyzer.ts b/lib/ai/smart-analyzer.ts index 3bde4b6ee..c22451481 100644 --- a/lib/ai/smart-analyzer.ts +++ b/lib/ai/smart-analyzer.ts @@ -33,7 +33,7 @@ export interface AnalysisResult { function inferDefaultImageEditSubIntent(text: string): AnalysisResult["subIntent"] { const lowerText = text.toLowerCase(); if (/去水印|水印|去logo|去\s*logo|移除logo|去除logo/.test(lowerText)) return "remove_watermark"; - if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) return "remove_background"; + if (/抠图|去背景|透明背景|去背|扣图|抠出|扣出|抠出来|扣出来|移除背景/.test(lowerText)) return "remove_background"; if (/换背景|改背景|背景换成|背景改成|白底|透明底/.test(lowerText)) return "replace_background"; if (/多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位/.test(lowerText)) return "angle_variant"; if (hasDirectAgentLocalEditIntent(text)) return "local_edit"; @@ -112,10 +112,10 @@ export function isExplicitImageGenerationRequest(text: string): boolean { const visualTarget = "(?:图|图片|海报|头像|插画|场景|主图|产品图|商品图|电商图|壁纸|封面|logo|图标)"; const createVisualTarget = new RegExp( - `(?:生成|创作|创建|制作|设计|做|出|画|绘制)${countToken}?张?.{0,32}${visualTarget}`, + `(?:生成|创作|创建|制作|设计|做|出|画(?!布)|绘制)${countToken}?张?.{0,32}${visualTarget}`, ); const drawSubject = new RegExp( - `(?:画|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`, + `(?:画(?!布)|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`, ); const continueVariants = new RegExp( `(?:再来|再出|再做|再给我|补|多来|继续生成|继续出)${countToken}?张`, @@ -141,7 +141,7 @@ export function isExplicitImageGenerationRequest(text: string): boolean { function isLikelyImageEditRequest(text: string): boolean { const normalizedText = text.toLowerCase(); - return hasDirectAgentLocalEditIntent(text) || /抠图|去背景|透明背景|去背|扣图|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test( + return hasDirectAgentLocalEditIntent(text) || /抠图|去背景|透明背景|去背|扣图|抠出|扣出|抠出来|扣出来|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test( normalizedText, ); } @@ -2331,7 +2331,7 @@ export function fallbackKeywordAnalysis( } // 抠图/去背景 - if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) { + if (/抠图|去背景|透明背景|去背|扣图|抠出|扣出|抠出来|扣出来|移除背景/.test(lowerText)) { return { intent: "image_edit", subIntent: "remove_background", diff --git a/lib/ai/specialists/index.ts b/lib/ai/specialists/index.ts index f91793f61..75827ad67 100644 --- a/lib/ai/specialists/index.ts +++ b/lib/ai/specialists/index.ts @@ -250,7 +250,7 @@ function normalizeEditCapability( subIntent: "remove_watermark", }; } - if (subIntent === "remove_background" || /抠图|去背|去背景|透明底/.test(normalizedText)) { + if (subIntent === "remove_background" || /抠图|去背|去背景|透明底|抠出|扣出|抠出来|扣出来/.test(normalizedText)) { return { capability: "remove_background", toolName: "removeBackground", diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index bb5d878e7..ee23944a0 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -48,9 +48,11 @@ import { import { shouldBypassCanvasAgentMissingTargetClarification } from "@/lib/ai/runtime/canvas-agent-clarification"; import { hasCanvasAgentRoutingContext } from "@/lib/ai/canvas-agent-routing"; import { + inferCanvasAgentImageEditCapability, resolveCanvasAgentRequestPolicy, resolveCanvasAgentTargetPreference, } from "@/lib/ai/runtime/canvas-agent-kernel"; +import { isExplicitImageGenerationRequest } from "@/lib/ai/smart-analyzer"; import { inferAgentImageCountFromText } from "@/lib/ai/runtime/image-count-intent"; import { buildRecentDialogueMemory, @@ -1463,6 +1465,64 @@ test.describe("canvas chat pure helpers", () => { expect(directContext?.targetImageUrl).toBe(canvasImageUrl); }); + test("routes canvas subject cutout wording to remove background instead of image generation", () => { + const canvasImageUrl = "https://cdn.example.com/canvas-dog.png"; + const userText = "帮我将画布中的狗的图扣出来"; + + expect(isExplicitImageGenerationRequest(userText)).toBe(false); + expect(inferCanvasAgentImageEditCapability(userText)).toBe("remove_background"); + + const directContext = resolveDirectAgentToolContext({ + execution: { + mode: "edit_image", + routeIntent: "image_edit", + routeSubIntent: "remove_background", + targetSource: "text_reference", + preferredCapability: "remove_background", + shouldUseStructuredReply: true, + shouldOptimizePrompt: true, + shouldApplyImagePipeline: true, + shouldApplyVideoPipeline: false, + shouldStayConversational: false, + shouldPublishToCanvas: false, + toolCalls: [], + availableTools: ["resolveTargetImage", "removeBackground"], + } as any, + requestResolvedMediaInput: { + imageRefs: [], + currentMessageImageRefs: [], + selectedImageRefs: [], + canvasImageRefs: [ + { + url: canvasImageUrl, + normalizedUrl: canvasImageUrl, + nodeId: "dog-node", + source: "canvas_context", + role: "reference", + }, + ], + historyUserImageRefs: [], + historyAssistantImageRefs: [], + resolvedImageRefs: [], + currentMessageImageUrls: [], + selectedImageUrls: [], + canvasImageUrls: [canvasImageUrl], + historyUserImageUrls: [], + historyAssistantImageUrls: [], + resolvedImageUrls: [], + selectedAssetIds: [], + candidateAssetIds: [], + uploadedAssetIds: [], + hasImage: true, + source: "canvas_context", + fromHistory: false, + } as any, + }); + + expect(directContext?.execution.toolName).toBe("removeBackground"); + expect(directContext?.targetImageUrl).toBe(canvasImageUrl); + }); + test("only runs direct image edit batches for explicit safe batch requests", () => { expect( shouldRunDirectAgentBatchImageEdit({