fix canvas cutout intent routing

This commit is contained in:
134 2026-07-07 21:38:24 +08:00
parent e2e373ce08
commit 51283b81fb
5 changed files with 82 additions and 26 deletions

View File

@ -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)) {

View File

@ -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:

View File

@ -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",

View File

@ -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",

View File

@ -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({