diff --git a/lib/ai/auto-agent-graph.ts b/lib/ai/auto-agent-graph.ts index a2d367ede..eab1db025 100644 --- a/lib/ai/auto-agent-graph.ts +++ b/lib/ai/auto-agent-graph.ts @@ -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, diff --git a/lib/ai/image-edit-subagent.ts b/lib/ai/image-edit-subagent.ts index 89f856731..e47c84685 100644 --- a/lib/ai/image-edit-subagent.ts +++ b/lib/ai/image-edit-subagent.ts @@ -66,6 +66,7 @@ export function buildImageEditSpecialistRoute(params: { "editTargetImage", "editImage", "removeBackground", + "removeWatermark", "enhanceImage", "extendImage", "resizeImage", diff --git a/lib/ai/runtime/direct-agent-execution.ts b/lib/ai/runtime/direct-agent-execution.ts index 65b121690..7dec89889 100644 --- a/lib/ai/runtime/direct-agent-execution.ts +++ b/lib/ai/runtime/direct-agent-execution.ts @@ -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": diff --git a/lib/ai/runtime/direct-agent-image-edit-executor.ts b/lib/ai/runtime/direct-agent-image-edit-executor.ts index 18f682f35..966cf6b77 100644 --- a/lib/ai/runtime/direct-agent-image-edit-executor.ts +++ b/lib/ai/runtime/direct-agent-image-edit-executor.ts @@ -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", diff --git a/lib/ai/runtime/intent-arbiter.ts b/lib/ai/runtime/intent-arbiter.ts index 5dff3053d..e22a23aaf 100644 --- a/lib/ai/runtime/intent-arbiter.ts +++ b/lib/ai/runtime/intent-arbiter.ts @@ -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": diff --git a/lib/ai/skill-router.ts b/lib/ai/skill-router.ts index 69f9e8dfe..b3d38116c 100644 --- a/lib/ai/skill-router.ts +++ b/lib/ai/skill-router.ts @@ -106,6 +106,7 @@ export function routeAgentSkill( "editTargetImage", "editImage", "removeBackground", + "removeWatermark", "enhanceImage", "extendImage", "resizeImage", diff --git a/lib/ai/smart-analyzer.ts b/lib/ai/smart-analyzer.ts index 2825ee8f8..23e133e8f 100644 --- a/lib/ai/smart-analyzer.ts +++ b/lib/ai/smart-analyzer.ts @@ -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)) { // 提取背景颜色 diff --git a/lib/ai/specialists/index.ts b/lib/ai/specialists/index.ts index fc3c5e649..85dc6ac14 100644 --- a/lib/ai/specialists/index.ts +++ b/lib/ai/specialists/index.ts @@ -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", diff --git a/lib/ai/tools-registry.ts b/lib/ai/tools-registry.ts index e84c2ac2e..00385ff80 100644 --- a/lib/ai/tools-registry.ts +++ b/lib/ai/tools-registry.ts @@ -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: "清晰增强", diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index 38fbc9aff..5832d8a1c 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -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,