From 6a73c876cc7989381346e37b6ef60a2ea9f85337 Mon Sep 17 00:00:00 2001 From: 134 Date: Wed, 21 Oct 2026 11:53:31 +0800 Subject: [PATCH] fix(agent): retire multi-angle tools and use reference-image generation for viewpoint edits --- .../canvas-agent-runtime/capability-access.ts | 14 ++++++--- .../image-access-tools.ts | 2 +- .../canvas-agent-runtime/legacy-tool-map.ts | 2 +- lib/ai/canvas-agent-runtime/prompt.ts | 1 + lib/ai/canvas-agent-runtime/tools.ts | 2 +- lib/ai/zcode-agent/task.ts | 1 + lib/canvas/capabilities/registry.ts | 4 ++- tests/routes/agent-capability-access.test.ts | 31 +++++++++++++++++-- ...as-agent-runtime-image-preferences.test.ts | 8 +++++ 9 files changed, 53 insertions(+), 12 deletions(-) diff --git a/lib/ai/canvas-agent-runtime/capability-access.ts b/lib/ai/canvas-agent-runtime/capability-access.ts index e49cb11dd..573d31bd7 100644 --- a/lib/ai/canvas-agent-runtime/capability-access.ts +++ b/lib/ai/canvas-agent-runtime/capability-access.ts @@ -123,7 +123,7 @@ export function getCanvasAgentAccessManifest( id: capability.id, workspace: "canvas", title: capability.title, - authorized: true, + authorized: capability.agent.exposed, previouslyExposed: !["image.add_to_chat", "image.layout_replace", "image.download"].includes( capability.id, @@ -146,7 +146,9 @@ export function getCanvasAgentAccessManifest( : "server", entry: capability.agent.toolName || capability.id.replaceAll(".", "_"), note: - capability.risk === "destructive" + capability.id === "image.multi_angle" + ? "旧多视角接口已废弃;转正面、换角度请使用 generateCanvasImages 并传入参考图片节点" + : capability.risk === "destructive" ? "需要用户明确要求及明确目标;模型不能自行确认" : capability.executionMode === "hybrid" ? "参数齐全时执行,否则打开真实交互面板" @@ -158,11 +160,13 @@ export function getCanvasAgentAccessManifest( id: `canvas.ui.${item.key}`, workspace: VIDEO_ONLY_AGENT_TOOLBAR_KEYS.has(item.key) ? "video" : "canvas", title: `${item.group} · ${item.label}`, - authorized: true, + authorized: item.key !== "imageMultiAngle", previouslyExposed: false, execution: "interactive", entry: "openCanvasFeature", - note: "操作当前可见的真实入口;隐藏入口保持隐藏,文件选择、支付、登录、发布和发送消息由用户完成", + note: item.key === "imageMultiAngle" + ? "旧多视角入口已废弃;换角度请使用 generateCanvasImages 并传入参考图片节点" + : "操作当前可见的真实入口;隐藏入口保持隐藏,文件选择、支付、登录、发布和发送消息由用户完成", }), ), ...IMAGE_CANVAS_OPERATIONS.map( @@ -262,6 +266,6 @@ export function getCanvasAgentAccessManifest( : undefined, }), ), - ].map(item => ({ ...item, authorized: isZcodeWorkspaceAuthorized(item.workspace), + ].map(item => ({ ...item, authorized: item.authorized && isZcodeWorkspaceAuthorized(item.workspace), ...(!isZcodeWorkspaceAuthorized(item.workspace) ? { note: ["用户暂未授权视频画布能力", item.note].filter(Boolean).join(";") } : {}) })); } diff --git a/lib/ai/canvas-agent-runtime/image-access-tools.ts b/lib/ai/canvas-agent-runtime/image-access-tools.ts index 674ed2ac2..b4e0dcb48 100644 --- a/lib/ai/canvas-agent-runtime/image-access-tools.ts +++ b/lib/ai/canvas-agent-runtime/image-access-tools.ts @@ -58,7 +58,7 @@ export function registerImageCanvasAccessTools(params: { const feature = getCanvasAgentAccessManifest().find( (item) => item.workspace === "canvas" && item.id === input.featureId, ); - if (!feature || !feature.id.startsWith("canvas.ui.")) + if (!feature?.authorized || !feature.id.startsWith("canvas.ui.")) throw new Error("请使用清单中的图片画布界面入口,媒体能力请使用对应专用工具"); const { nodes } = await readOwnedCanvas(params.userId, params.projectId); if (input.nodeIds.some((nodeId) => !nodes.some((node) => node.id === nodeId))) diff --git a/lib/ai/canvas-agent-runtime/legacy-tool-map.ts b/lib/ai/canvas-agent-runtime/legacy-tool-map.ts index 98d8bd275..681b1984c 100644 --- a/lib/ai/canvas-agent-runtime/legacy-tool-map.ts +++ b/lib/ai/canvas-agent-runtime/legacy-tool-map.ts @@ -5,6 +5,6 @@ export const LEGACY_AGENT_TOOL_MAP: Record = { analyzeTargetImage:"analyzeCanvasImages", analyzeImage:"analyzeCanvasImages", generateNewImage:"generateCanvasImages", generateImage:"generateCanvasImages", editTargetImage:"editTargetImage", editImage:"editTargetImage", publishResultToCanvas:"editCanvasNodes", publishToCanvas:"editCanvasNodes", removeBackground:"removeBackground", removeWatermark:"removeWatermark", enhanceImage:"enhanceImage", extendImage:"extendImage", resizeImage:"resizeImage", - angleEditImage:"angleEditImage", localEditImage:"localEditImage", boxCutout:"boxCutout", useSelectedCanvasImage:"readCanvas", + angleEditImage:"retired:use-generateCanvasImages-with-reference", localEditImage:"localEditImage", boxCutout:"boxCutout", useSelectedCanvasImage:"readCanvas", useLatestResultImage:"readCanvas", useOriginalUploadImage:"readCanvas", }; diff --git a/lib/ai/canvas-agent-runtime/prompt.ts b/lib/ai/canvas-agent-runtime/prompt.ts index 6f59f59de..8ccd3bd13 100644 --- a/lib/ai/canvas-agent-runtime/prompt.ts +++ b/lib/ai/canvas-agent-runtime/prompt.ts @@ -14,6 +14,7 @@ export function canvasRuntimeSystemPrompt( "用户说第几张、左边、右边时结合画布实际位置和选中顺序定位,不能猜测不存在的节点。", "图片分析请使用 analyzeCanvasImages,不要把文件名当成图片观察结果。", "需要新图或背景重绘时用 generateCanvasImages;单纯改尺寸、抠图等优先使用对应工具。", + "转正面、换角度、旋转主体视角也使用 generateCanvasImages,传入真实 referenceNodeIds 和具体目标视角,保持产品结构与其他未要求修改的细节。旧多视角功能和 angleEditImage 已废弃,不得调用旧接口或打开旧面板。", "最终交付的张数不包括中间产物。保留所有原图,结果放到原图旁边;不要删除、覆盖或偷偷移动原图。", "同一张图片需要前一步结果时必须等待工具返回;彼此独立的图片可在同轮调用不同工具。", "严禁声称未实际执行的操作已完成。交互面板已打开、任务已排队都不代表图片已完成。", diff --git a/lib/ai/canvas-agent-runtime/tools.ts b/lib/ai/canvas-agent-runtime/tools.ts index 43553d6dd..5bb56959a 100644 --- a/lib/ai/canvas-agent-runtime/tools.ts +++ b/lib/ai/canvas-agent-runtime/tools.ts @@ -178,7 +178,7 @@ export async function buildCanvasRuntimeTools(params: { register( "generateCanvasImages", "生成图片", - "使用本站配置的生图模型生成或修改图片。通过 referenceNodeIds 提供参考图;结果作为新节点放在参考图旁边,保留原图。", + "使用本站配置的生图模型生成或修改图片,包括转正面、换角度、旋转主体视角。通过 referenceNodeIds 提供参考图;结果作为新节点放在参考图旁边,保留原图。", z.object({ prompt: z.string().min(1).max(8000), referenceNodeIds: z.array(z.string().min(1).max(180)).max(8).default([]), diff --git a/lib/ai/zcode-agent/task.ts b/lib/ai/zcode-agent/task.ts index 260ae5518..b4efd2ff0 100644 --- a/lib/ai/zcode-agent/task.ts +++ b/lib/ai/zcode-agent/task.ts @@ -57,6 +57,7 @@ export async function runZcodeCanvasTask(ports: Ports): Promise { - expect(CANVAS_CAPABILITY_REGISTRY.every((item) => item.agent.exposed)).toBe(true); +test("supported image capabilities remain authorized while retired multi-angle tools are unavailable", async () => { + expect(CANVAS_CAPABILITY_REGISTRY.filter(item => !item.agent.exposed).map(item => item.id)).toEqual(["image.multi_angle"]); + expect(CANVAS_CAPABILITY_REGISTRY.find(item => item.id === "image.multi_angle")?.mcp.exposed).toBe(false); expect(CANVAS_CAPABILITY_IDS).toHaveLength(25); }); +test("retired angle tools are absent even with explicit targets and toolbar visibility enabled", async () => { + const load = serverLoader({ + "@/lib/db/queries": { getSystemSetting: async () => null }, + "@/lib/canvas/capabilities/runtime": { executeCanvasCapabilityRuntime: async () => { throw new Error("unexpected execution"); } }, + }); + const { createCanvasCapabilityTools } = load("lib/canvas/capabilities/ai-sdk-tools.ts"); + const tools = await createCanvasCapabilityTools({ projectId: "p", userId: "u", requestUrl: "https://example.test", requestHeaders: new Headers(), selectedNodeIds: ["reference"], allowExplicitTargets: true }); + expect(tools.angleEditImage).toBeUndefined(); + expect(tools.editTargetImage).toBeDefined(); + const { isCanvasCapabilityEnabled } = load("lib/canvas/capabilities/availability.ts"); + const { getCanvasCapability } = load("lib/canvas/capabilities/registry.ts"); + expect(await isCanvasCapabilityEnabled(getCanvasCapability("image.multi_angle"), "agent")).toBe(false); +}); +test("Agent cannot open the retired angle panel through a generic UI command", async () => { + const events: unknown[] = [], definitions: any = {}, executable: any = {}; + const load = serverLoader({ "@/lib/canvas/agent-mutations": {}, "./canvas": {} }); + load("lib/ai/canvas-agent-runtime/image-access-tools.ts").registerImageCanvasAccessTools({ userId: "u", projectId: "p", runId: "r", selectedNodeIds: ["reference"], definitions, executable, emit: (...args: unknown[]) => events.push(args) }); + await expect(executable.openCanvasFeature.execute({ featureId: "canvas.ui.imageMultiAngle" }, "old-panel")).rejects.toThrow("图片画布界面入口"); + expect(events).toHaveLength(0); +}); test("access manifest covers all configured video functions and node/UI actions without granting fake backends", () => { const manifest = getCanvasAgentAccessManifest(); expect(new Set(manifest.map((item) => item.id)).size).toBe(manifest.length); - expect(manifest.filter(item => item.workspace === "canvas").every(item => item.authorized)).toBe(true); + expect(manifest.filter(item => item.workspace === "canvas" && !item.authorized).map(item => item.id)).toEqual(["image.multi_angle", "canvas.ui.imageMultiAngle"]); + const retired = summarizeAgentCapabilities(manifest).find(item => item.id === "image.multi_angle"); + expect(retired).not.toHaveProperty("entry"); + expect(manifest.find(item => item.id === "image.multi_angle")?.note).toContain("generateCanvasImages"); expect(manifest.filter(item => item.workspace === "video").every(item => !item.authorized)).toBe(true); for (const key of Object.keys(defaultVideoCanvasFunctionConfig.functions)) expect(manifest.some((item) => item.id === `video.function.${key}`)).toBe(true); diff --git a/tests/routes/canvas-agent-runtime-image-preferences.test.ts b/tests/routes/canvas-agent-runtime-image-preferences.test.ts index 0ac96f636..8406aa84f 100644 --- a/tests/routes/canvas-agent-runtime-image-preferences.test.ts +++ b/tests/routes/canvas-agent-runtime-image-preferences.test.ts @@ -119,3 +119,11 @@ test("an unavailable planner model recovers automatically with the user's image expect(h.generated).toHaveLength(1); expect(h.generated[0]).toMatchObject({ model: "system-default", imageSize: "4K", aspectRatio: "3:4", allowFailover: true, agentImageMode: "auto" }); }); + +test("front-view edits use reference-image generation with the user's selected model and parameters", async () => { + const h = harness(); const tools = await h.build(); + await tools.executable.generateCanvasImages.execute({ prompt: "把柜子角度改为正面,保持柜子结构和材质", referenceNodeIds: ["reference"] }, "front-view"); + expect(h.generated).toHaveLength(1); + expect(h.generated[0]).toMatchObject({ prompt: "把柜子角度改为正面,保持柜子结构和材质", referenceImages: ["https://test.example/reference.png"], model: "user-image-model", aspectRatio: "3:4", imageSize: "4K", imageCount: 3, allowFailover: false }); + expect(h.operations[0].plan.preserveOriginal).toBe(true); +});