feat: route canvas agent watermark removal

This commit is contained in:
134 2026-06-23 23:31:43 +08:00
parent 9151058d79
commit 0afa404b19
10 changed files with 238 additions and 21 deletions

View File

@ -77,6 +77,7 @@ export type AgentPreferredCapability =
| "analyze"
| "generic_edit"
| "remove_background"
| "remove_watermark"
| "enhance"
| "extend"
| "resize"
@ -246,6 +247,7 @@ function inferPreferredCapability(
tool.name !== "useOriginalUploadImage"
)?.name || toolCalls?.[0]?.name;
if (firstTool === "removeBackground") return "remove_background";
if (firstTool === "removeWatermark") return "remove_watermark";
if (firstTool === "enhanceImage") return "enhance";
if (firstTool === "extendImage") return "extend";
if (firstTool === "resizeImage") return "resize";
@ -263,6 +265,8 @@ function inferPreferredCapability(
switch (analysis.subIntent) {
case "remove_background":
return "remove_background";
case "remove_watermark":
return "remove_watermark";
case "enhance":
return "enhance";
case "extend":
@ -1395,6 +1399,7 @@ function sanitizeAgentPlan(
"analyze",
"generic_edit",
"remove_background",
"remove_watermark",
"enhance",
"extend",
"resize",
@ -1567,7 +1572,7 @@ async function buildExecutionPlanWithAgent(
- respond:纯对话、策略讨论、解释说明、普通问答
- clarify:信息不足,需要先澄清
- analyze_image:用户要你看图、识图、评估、分析、给建议
- edit_image:基于当前图/历史图做修改、抠图、增强、换背景、调色、尺寸调整、局部重绘
- edit_image:基于当前图/历史图做修改、抠图、去水印、增强、换背景、调色、尺寸调整、局部重绘
- generate_image:生成新图
- generate_video:生成视频
@ -1579,7 +1584,7 @@ async function buildExecutionPlanWithAgent(
5. 优先保持一次只选择一个主工具;toolCalls 只从提供的工具列表里选。
6. shouldUseStructuredReply 只在 edit_image / generate_image / generate_video 时设为 true。
7. targetSource 要忠实反映当前目标图来源:explicit_selection/current_upload/text_reference/working_memory/latest_result/clarification/none。
8. preferredCapability 要尽量具体,例如 remove_background / enhance / extend / resize / box_cutout / publish_canvas。
8. preferredCapability 要尽量具体,例如 remove_background / remove_watermark / enhance / extend / resize / box_cutout / publish_canvas。
9. 必须优先使用新的强语义工具名:resolveTargetImage / analyzeTargetImage / editTargetImage / generateNewImage / publishResultToCanvas。
10. 必须遵守当前专家路由约束;如果约束要求纯对话,则绝不能输出 generate_image / edit_image / generate_video。
@ -1590,7 +1595,7 @@ async function buildExecutionPlanWithAgent(
"intent": "chat|vision|image_edit|image_gen|video_gen",
"subIntent": "可选,仅 image_edit 时填写",
"targetSource": "explicit_selection|current_upload|text_reference|working_memory|latest_result|clarification|none",
"preferredCapability": "respond|analyze|generic_edit|remove_background|enhance|extend|resize|box_cutout|generate_image|generate_video|publish_canvas",
"preferredCapability": "respond|analyze|generic_edit|remove_background|remove_watermark|enhance|extend|resize|box_cutout|generate_image|generate_video|publish_canvas",
"responseMode": "direct_answer|tool_then_answer|tool_then_execute|clarify",
"shouldUseHistoryImage": true,
"shouldUseStructuredReply": false,

View File

@ -66,6 +66,7 @@ export function buildImageEditSpecialistRoute(params: {
"editTargetImage",
"editImage",
"removeBackground",
"removeWatermark",
"enhanceImage",
"extendImage",
"resizeImage",

View File

@ -12,6 +12,7 @@ export type DirectAgentToolCapability =
| "analyze"
| "generic_edit"
| "remove_background"
| "remove_watermark"
| "enhance"
| "resize"
| "extend"
@ -23,6 +24,7 @@ export type DirectAgentToolName =
| "analyzeImage"
| "editImage"
| "removeBackground"
| "removeWatermark"
| "enhanceImage"
| "resizeImage"
| "extendImage"
@ -88,6 +90,8 @@ function mapDirectToolNameToCapability(
return "generic_edit";
case "removeBackground":
return "remove_background";
case "removeWatermark":
return "remove_watermark";
case "enhanceImage":
return "enhance";
case "resizeImage":
@ -171,6 +175,35 @@ export function resolveDirectAgentToolExecution(
const toolNames = mergedToolCalls.map((tool) => String(tool.name || ""));
const toolNameSet = new Set(toolNames);
const hasToolCall = (toolName: string) => toolNameSet.has(toolName);
const preferredSpecificImageEditTool = (() => {
const preferredCapability = execution?.preferredCapability || plan?.preferredCapability;
const routeSubIntent = execution?.routeSubIntent || plan?.subIntent;
if (
preferredCapability === "remove_background" ||
routeSubIntent === "remove_background"
) {
return "removeBackground";
}
if (
preferredCapability === "remove_watermark" ||
routeSubIntent === "remove_watermark"
) {
return "removeWatermark";
}
if (preferredCapability === "enhance" || routeSubIntent === "enhance") {
return "enhanceImage";
}
if (preferredCapability === "resize" || routeSubIntent === "resize") {
return "resizeImage";
}
if (preferredCapability === "extend" || routeSubIntent === "extend") {
return "extendImage";
}
if (preferredCapability === "box_cutout" || routeSubIntent === "box_cutout") {
return "boxCutout";
}
return null;
})();
const sourceTools = toolNames.filter((toolName): toolName is DirectAgentSourceToolName =>
toolName === "useSelectedCanvasImage" ||
toolName === "useLatestResultImage" ||
@ -211,21 +244,23 @@ export function resolveDirectAgentToolExecution(
? "analyzeImage"
: hasToolCall("removeBackground")
? "removeBackground"
: hasToolCall("enhanceImage")
? "enhanceImage"
: hasToolCall("resizeImage")
? "resizeImage"
: hasToolCall("extendImage")
? "extendImage"
: hasToolCall("boxCutout")
? "boxCutout"
: hasToolCall("editTargetImage") || hasToolCall("editImage")
? "editImage"
: hasToolCall("generateNewImage") || hasToolCall("generateImage")
? "generateImage"
: hasToolCall("generateVideo")
? "generateVideo"
: null) ||
: hasToolCall("removeWatermark")
? "removeWatermark"
: hasToolCall("enhanceImage")
? "enhanceImage"
: hasToolCall("resizeImage")
? "resizeImage"
: hasToolCall("extendImage")
? "extendImage"
: hasToolCall("boxCutout")
? "boxCutout"
: hasToolCall("editTargetImage") || hasToolCall("editImage")
? preferredSpecificImageEditTool || "editImage"
: hasToolCall("generateNewImage") || hasToolCall("generateImage")
? "generateImage"
: hasToolCall("generateVideo")
? "generateVideo"
: null) ||
(() => {
if (
execution?.preferredCapability === "analyze" ||
@ -240,6 +275,12 @@ export function resolveDirectAgentToolExecution(
) {
return "removeBackground";
}
if (
execution?.preferredCapability === "remove_watermark" ||
execution?.routeSubIntent === "remove_watermark"
) {
return "removeWatermark";
}
if (
execution?.preferredCapability === "enhance" ||
execution?.routeSubIntent === "enhance"
@ -325,6 +366,7 @@ export function isDirectAgentImageEditToolName(
| "editImage"
| "enhanceImage"
| "removeBackground"
| "removeWatermark"
| "resizeImage"
| "extendImage"
| "boxCutout" {
@ -332,6 +374,7 @@ export function isDirectAgentImageEditToolName(
case "editImage":
case "enhanceImage":
case "removeBackground":
case "removeWatermark":
case "resizeImage":
case "extendImage":
case "boxCutout":
@ -349,6 +392,8 @@ export function getDirectAgentToolLabel(capability: DirectAgentToolCapability):
return "Agent工具:图片编辑";
case "remove_background":
return "Agent工具:抠图";
case "remove_watermark":
return "Agent工具:去水印";
case "enhance":
return "Agent工具:图像增强";
case "resize":
@ -392,6 +437,8 @@ export function getDirectAgentToolSuccessMessage(
return "已完成图片编辑,结果如下:";
case "remove_background":
return "已完成抠图处理,结果如下:";
case "remove_watermark":
return "已完成去水印处理,结果如下:";
case "enhance":
return "已完成图像增强,结果如下:";
case "resize":
@ -419,6 +466,8 @@ export function getDirectAgentToolTargetMissingMessage(
return "请先上传或选中一张图片,我才能继续帮你修改。";
case "remove_background":
return "请先上传或选中一张图片,我才能继续帮你执行抠图。";
case "remove_watermark":
return "请先上传或选中一张图片,我才能继续帮你去水印。";
case "enhance":
return "请先上传或选中一张图片,我才能继续帮你做图像增强。";
case "resize":
@ -455,6 +504,8 @@ export function getDirectAgentToolErrorMessage(
return `图片编辑暂时失败:${detail}`;
case "remove_background":
return `抠图暂时失败:${detail}`;
case "remove_watermark":
return `去水印暂时失败:${detail}`;
case "enhance":
return `图像增强暂时失败:${detail}`;
case "resize":

View File

@ -141,6 +141,37 @@ async function executeDirectAgentEnhanceJob(params: {
};
}
async function executeDirectAgentWatermarkRemoveJob(params: {
requestUrl: string;
requestHeaders: Headers;
imageUrl: string;
}) {
const endpoint = new URL("/api/watermark-remove", params.requestUrl).toString();
const cookieHeader = params.requestHeaders.get("cookie") || "";
const result = await requestLongTaskResult({
endpoint,
payload: {
image_url: params.imageUrl,
},
timeoutMs: 20 * 60 * 1000,
headers: cookieHeader
? {
cookie: cookieHeader,
}
: undefined,
});
if (!result?.success || !result?.original_url) {
throw new Error(String(result?.error || result?.detail || "去水印失败"));
}
return {
originalUrl: String(result.original_url),
thumbnailUrl: String(result.thumbnail_url || result.original_url),
imageSize: "2K",
};
}
export function isDirectAgentImageEditServiceError(error: unknown) {
return (
error instanceof RmbgServiceError ||
@ -315,6 +346,23 @@ export async function executeDirectAgentImageEditTool(
});
}
if (params.capability === "remove_watermark") {
const result = await executeDirectAgentWatermarkRemoveJob({
requestUrl: params.requestUrl,
requestHeaders: params.requestHeaders,
imageUrl: params.sourceImageUrl,
});
return finalizeDirectAgentImageEditResult({
userId: params.userId,
capability: params.capability,
originalUrl: result.originalUrl,
thumbnailUrl: result.thumbnailUrl,
imageSize: result.imageSize,
mediaAssetId: null,
});
}
if (params.capability === "resize") {
const plannedArgs = findAgentToolCallArgs(
"resizeImage",

View File

@ -18,6 +18,7 @@ export type IntentArbiterPreferredCapability =
| "analyze"
| "generic_edit"
| "remove_background"
| "remove_watermark"
| "enhance"
| "extend"
| "resize"
@ -113,13 +114,14 @@ function isExplicitImageGenerationRequest(text: string): boolean {
function isLikelyImageEditRequest(text: string): boolean {
const normalizedText = String(text || "").toLowerCase();
return /抠图|去背景|透明背景|去背|扣图|移除背景|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
return /抠图|去背景|透明背景|去背|扣图|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
normalizedText,
);
}
function inferDefaultImageEditSubIntent(text: string): string {
const lowerText = String(text || "").toLowerCase();
if (/去水印|水印|去logo|去\s*logo|移除logo|去除logo/.test(lowerText)) return "remove_watermark";
if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) return "remove_background";
if (/换背景|改背景|背景换成|背景改成|白底|透明底/.test(lowerText)) return "replace_background";
if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|比例|画幅|竖版|横版|竖图|横图|9:16|16:9|1:1/.test(lowerText)) return "extend";
@ -161,6 +163,8 @@ function mapPreferredCapability(
switch (subIntent) {
case "remove_background":
return "remove_background";
case "remove_watermark":
return "remove_watermark";
case "extend":
return "extend";
case "resize":

View File

@ -106,6 +106,7 @@ export function routeAgentSkill(
"editTargetImage",
"editImage",
"removeBackground",
"removeWatermark",
"enhanceImage",
"extendImage",
"resizeImage",

View File

@ -31,6 +31,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 "replace_background";
if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|比例|画幅|竖版|横版|竖图|横图|9:16|16:9|1:1/.test(lowerText)) return "extend";
@ -137,7 +138,7 @@ export function isExplicitImageGenerationRequest(text: string): boolean {
function isLikelyImageEditRequest(text: string): boolean {
const normalizedText = text.toLowerCase();
return /抠图|去背景|透明背景|去背|扣图|移除背景|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
return /抠图|去背景|透明背景|去背|扣图|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
normalizedText,
);
}
@ -383,6 +384,7 @@ const ANALYZER_SYSTEM_PROMPT = `你是一个友好的AI多模态Agent。用户
### image_edit - 图片编辑(用户上传了图片并想编辑)
子意图:
- remove_background: 抠图、去背景、透明背景
- remove_watermark: 去水印、移除图片中的水印或 logo 水印
- replace_background: 换背景、改背景、白底
- enhance: 变清晰、高清、锐化、降噪
- extend: 扩展、拓展、延伸画布
@ -766,7 +768,7 @@ async function analyzeWithHardcodedQwen3VlFallback(params: {
const prompt = [
"你是图片需求分析助手。请结合图片与用户意图,输出严格 JSON,不要输出其他内容。",
"intent 只能是 image_edit / vision / image_gen / video_gen / chat。",
"subIntent 可选:remove_background / replace_background / enhance / extend / inpaint / style_transfer / color_adjust / redo / resize。",
"subIntent 可选:remove_background / remove_watermark / replace_background / enhance / extend / inpaint / style_transfer / color_adjust / redo / resize。",
"如果用户提到视频、首帧、尾帧、分镜,intent=video_gen。",
"有图片且用户在修改图片时,优先 intent=image_edit。",
"JSON 字段:intent, subIntent, confidence, conversationalReply, needsClarification, clarifyingQuestions。",
@ -2333,6 +2335,16 @@ export function fallbackKeywordAnalysis(
};
}
// 去水印
if (/去水印|水印|去logo|去\s*logo|移除logo|去除logo/.test(lowerText)) {
return {
intent: "image_edit",
subIntent: "remove_watermark",
confidence: 0.9,
conversationalReply: "好的,我来帮你去掉图片里的水印,并保留原图~",
};
}
// 换背景
if (/换背景|白底|改背景|背景换成|纯色背景|换成.*背景|替换.*背景/.test(lowerText)) {
// 提取背景颜色

View File

@ -224,6 +224,13 @@ function normalizeEditCapability(
const normalizedText = String(text || "").toLowerCase();
const subIntent = String(analysis.subIntent || "").trim().toLowerCase();
if (subIntent === "remove_watermark" || /去水印|水印|去logo|去\s*logo|移除logo|去除logo/.test(normalizedText)) {
return {
capability: "remove_watermark",
toolName: "removeWatermark",
subIntent: "remove_watermark",
};
}
if (subIntent === "remove_background" || /抠图|去背|去背景|透明底/.test(normalizedText)) {
return {
capability: "remove_background",

View File

@ -19,6 +19,7 @@ export type AgentProjectToolName =
| "generateImage"
| "editImage"
| "removeBackground"
| "removeWatermark"
| "enhanceImage"
| "extendImage"
| "resizeImage"
@ -166,6 +167,14 @@ const INTERNAL_AGENT_TOOLS: AgentProjectToolDefinition[] = [
requiresImage: true,
producesMedia: true,
},
{
name: "removeWatermark",
title: "去水印",
description: "清理图片中的水印、Logo 水印或遮挡瑕疵,结果作为新版本保留原图。",
category: "image",
requiresImage: true,
producesMedia: true,
},
{
name: "enhanceImage",
title: "清晰增强",

View File

@ -122,6 +122,11 @@ import {
shouldPreserveCanvasAgentSlotOnlyContext,
shouldBypassCanvasImagePointsPrecheckForAgentControl,
} from "@/lib/canvas/agent-client-intent";
import {
getDirectAgentToolLabel,
getDirectAgentToolSuccessMessage,
resolveDirectAgentToolExecution,
} from "@/lib/ai/runtime/direct-agent-execution";
import {
applyCanvasOperationPlanToCanvasData,
CanvasAgentMutationError,
@ -1015,6 +1020,80 @@ test.describe("canvas chat pure helpers", () => {
});
});
test("routes watermark removal through the dedicated direct image tool", () => {
const result = resolveDirectAgentToolExecution({
mode: "edit_image",
routeIntent: "image_edit",
routeSubIntent: "remove_watermark",
targetSource: "explicit_selection",
preferredCapability: "remove_watermark",
shouldUseStructuredReply: true,
shouldOptimizePrompt: true,
shouldApplyImagePipeline: true,
shouldApplyVideoPipeline: false,
shouldStayConversational: false,
shouldPublishToCanvas: false,
toolCalls: [
{
name: "resolveTargetImage",
args: { source: "selected_canvas" },
},
{
name: "editTargetImage",
reason: "planner only provided the generic edit tool",
},
],
availableTools: ["resolveTargetImage", "editTargetImage", "removeWatermark"],
});
expect(result).toMatchObject({
toolName: "removeWatermark",
capability: "remove_watermark",
sourcePreferences: ["selected_canvas"],
});
expect(getDirectAgentToolLabel("remove_watermark")).toBe("Agent工具:去水印");
expect(getDirectAgentToolSuccessMessage("remove_watermark")).toBe(
"已完成去水印处理,结果如下:"
);
});
test("plans watermark removal results as non-destructive canvas revisions", () => {
const result = buildCanvasAgentDirectWritebackPlan({
projectId: plannerContext.projectId || "",
chatId: "chat-1",
assistantMessageId: "assistant-watermark",
operationLabel: "agent-remove_watermark",
resultType: "image",
resultUrls: ["https://cdn.example.com/watermark-removed.png"],
prompt: "把选中图片上的水印去掉",
requestTextContent: "把选中图片上的水印去掉,结果放右边",
mode: "edit",
context: plannerContext,
targetRefs: [{ nodeId: "image-1" }],
sourceImageUrls: ["https://cdn.example.com/source-watermarked.png"],
});
expect(result).toMatchObject({
operation: "append_revision",
shouldFillSlot: false,
mediaItemIds: ["assistant-watermark:agent-remove_watermark:1"],
plan: {
operation: "append_revision",
anchorNodeId: "image-1",
placement: "right",
preserveOriginal: true,
},
});
expect(result?.mediaItems[0]).toMatchObject({
type: "image",
url: "https://cdn.example.com/watermark-removed.png",
sourceNodeIds: ["image-1"],
metadata: {
sourceImageUrls: ["https://cdn.example.com/source-watermarked.png"],
},
});
});
test("anchors direct image edits to resolved target refs before fallback selection", () => {
const context: CanvasAgentContext = {
...plannerContext,