From e9921cae98be88f58065c72f119f1391bff88b62 Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Thu, 5 Nov 2026 15:49:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E7=95=99=E4=B8=8D=E5=90=8C=E6=A1=86?= =?UTF-8?q?=E9=80=89=E5=85=A5=E5=8F=A3=E7=9A=84=E8=AE=A1=E8=B4=B9=E8=A7=84?= =?UTF-8?q?=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/canvas/capabilities/headless-executor.ts | 45 +++++++++++-------- ...as-agent-runtime-image-preferences.test.ts | 2 +- tests/routes/rmbg-delivery.test.ts | 22 ++++++++- 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/lib/canvas/capabilities/headless-executor.ts b/lib/canvas/capabilities/headless-executor.ts index cdd5f6b23..2fcf0790f 100644 --- a/lib/canvas/capabilities/headless-executor.ts +++ b/lib/canvas/capabilities/headless-executor.ts @@ -1,3 +1,4 @@ +import { executeBoxCutoutJob } from "@/lib/ai/box-cutout/service"; import { executeExtendJob } from "@/lib/ai/extend/service"; import { executeLayerSplitJob } from "@/lib/ai/layer-split/service"; import { executeResizeJob } from "@/lib/ai/resize/service"; @@ -186,25 +187,31 @@ async function executeForTarget( } if (command.capabilityId === "image.box_cutout") { - const response = await executeBoxCutoutPOST(new Request(params.requestUrl, { - method: "POST", - headers: { "Content-Type": "application/json", "X-Request-ID": command.requestId }, - body: JSON.stringify({ - imageUrl: target.sourceImageUrl, - sourceNodeId: target.nodeId, - cropRect: { - x: asFiniteNumber(command.args.x), - y: asFiniteNumber(command.args.y), - width: asFiniteNumber(command.args.width), - height: asFiniteNumber(command.args.height), - }, - paddingRatio: asFiniteNumber(command.args.paddingRatio), - prompt: prompt || "框选抠图", - }), - }), params.userId); - const result = await response.json(); - if (!response.ok || !result.success) { - throw new RmbgServiceError(result.detail || result.message || result.error || "框选抠图未能完成", response.status); + const job = { + imageUrl: target.sourceImageUrl, + sourceNodeId: target.nodeId, + cropRect: { + x: asFiniteNumber(command.args.x), + y: asFiniteNumber(command.args.y), + width: asFiniteNumber(command.args.width), + height: asFiniteNumber(command.args.height), + }, + paddingRatio: asFiniteNumber(command.args.paddingRatio), + prompt: prompt || "框选抠图", + }; + let result; + if (command.source === "toolbar") { + const response = await executeBoxCutoutPOST(new Request(params.requestUrl, { + method: "POST", + headers: { "Content-Type": "application/json", "X-Request-ID": command.requestId }, + body: JSON.stringify(job), + }), params.userId); + result = await response.json(); + if (!response.ok || !result.success) { + throw new RmbgServiceError(result.detail || result.message || result.error || "框选抠图未能完成", response.status); + } + } else { + result = await executeBoxCutoutJob({ ...job, requestId: command.requestId, userId: params.userId }); } return [{ ...common, diff --git a/tests/routes/canvas-agent-runtime-image-preferences.test.ts b/tests/routes/canvas-agent-runtime-image-preferences.test.ts index 53aa39b1e..8b07e6237 100644 --- a/tests/routes/canvas-agent-runtime-image-preferences.test.ts +++ b/tests/routes/canvas-agent-runtime-image-preferences.test.ts @@ -102,7 +102,7 @@ test("mixed-model batches bill and label only the delivered images using their a test("headless image features cannot substitute their default when a pinned model disappears", async () => { let generated = 0; const mocks: Record = { - "@/lib/canvas/operations/box-cutout": {}, "@/lib/ai/extend/service": {}, "@/lib/ai/layer-split/service": {}, "@/lib/ai/resize/service": {}, "@/lib/ai/rmbg/service": {}, + "@/lib/canvas/operations/box-cutout": {}, "@/lib/ai/box-cutout/service": {}, "@/lib/ai/extend/service": {}, "@/lib/ai/layer-split/service": {}, "@/lib/ai/resize/service": {}, "@/lib/ai/rmbg/service": {}, "@/lib/canvas/capabilities/internal-endpoint": {}, "@/lib/client/long-task": {}, "@/lib/canvas/generate-image": { generateCanvasImage: async () => { generated++; return { images: [] }; } }, "@/lib/style-pro/model-options": { getStyleProImageModelEntries: async () => [{ model: { id: "default", modelId: "default", isDefault: true } }], sortStyleProImageModelEntries: (entries: any[]) => entries, findStyleProImageModelByRef: () => null }, diff --git a/tests/routes/rmbg-delivery.test.ts b/tests/routes/rmbg-delivery.test.ts index e06025167..7cb4b98ae 100644 --- a/tests/routes/rmbg-delivery.test.ts +++ b/tests/routes/rmbg-delivery.test.ts @@ -68,6 +68,7 @@ test("headless box cutout keeps its task identity, pixel bounds and input-reject let rejectInput = false; const bounds = { left: 20, top: 30, width: 200, height: 300 }; const executor = serverLoader({ + "@/lib/ai/box-cutout/service": {}, "@/lib/canvas/operations/box-cutout": { executeBoxCutoutPOST: async (request: Request, userId: string) => { calls++; expect(userId).toBe("user-a"); @@ -81,7 +82,7 @@ test("headless box cutout keeps its task identity, pixel bounds and input-reject "@/lib/canvas/capabilities/internal-endpoint": {}, "@/lib/client/long-task": {}, "@/lib/canvas/generate-image": {}, "@/lib/style-pro/model-options": {}, })("lib/canvas/capabilities/headless-executor.ts"); - const params = { userId: "user-a", requestUrl: "https://example.com/api/canvas/project-a/capabilities", requestHeaders: new Headers(), command: { requestId: "box-a", capabilityId: "image.box_cutout", args: { x: 0.1, y: 0.2, width: 0.3, height: 0.4, paddingRatio: 0.08 } }, targets: [{ nodeId: "source", title: "原图", sourceImageUrl: `${cdn}/original.png` }] }; + const params = { userId: "user-a", requestUrl: "https://example.com/api/canvas/project-a/capabilities", requestHeaders: new Headers(), command: { requestId: "box-a", capabilityId: "image.box_cutout", source: "toolbar", args: { x: 0.1, y: 0.2, width: 0.3, height: 0.4, paddingRatio: 0.08 } }, targets: [{ nodeId: "source", title: "原图", sourceImageUrl: `${cdn}/original.png` }] }; const results = await executor.executeCanvasCapabilityHeadlessly(params); expect(calls).toBe(1); expect(results).toMatchObject([{ sourceNodeId: "source", url: `${cdn}/cutout.png`, metadata: { editMode: "box-cutout", paddedCropRectPixels: bounds } }]); @@ -90,6 +91,25 @@ test("headless box cutout keeps its task identity, pixel bounds and input-reject expect(calls).toBe(2); }); +test("Agent and MCP box cutouts retain their existing billing path", async () => { + let jobs = 0, featureCharges = 0; + const executor = serverLoader({ + "@/lib/ai/box-cutout/service": { executeBoxCutoutJob: async (params: any) => { + jobs++; + expect(params).toMatchObject({ userId: "user-a", requestId: "box-agent", sourceNodeId: "source" }); + return { original_url: `${cdn}/cutout.png` }; + } }, + "@/lib/canvas/operations/box-cutout": { executeBoxCutoutPOST: async () => { featureCharges++; throw new Error("Unexpected toolbar charge"); } }, + "@/lib/ai/extend/service": {}, "@/lib/ai/layer-split/service": {}, "@/lib/ai/resize/service": {}, "@/lib/ai/rmbg/service": {}, + "@/lib/canvas/capabilities/internal-endpoint": {}, "@/lib/client/long-task": {}, + "@/lib/canvas/generate-image": {}, "@/lib/style-pro/model-options": {}, + })("lib/canvas/capabilities/headless-executor.ts"); + for (const source of ["agent", "mcp", "system"]) { + await executor.executeCanvasCapabilityHeadlessly({ userId: "user-a", command: { requestId: "box-agent", capabilityId: "image.box_cutout", source, args: { x: 0, y: 0, width: 0.5, height: 0.5 } }, targets: [{ nodeId: "source", title: "原图", sourceImageUrl: `${cdn}/source.png` }] }); + } + expect({ jobs, featureCharges }).toEqual({ jobs: 3, featureCharges: 0 }); +}); + function rmbgLoader(fetch: typeof globalThis.fetch, timer?: typeof setTimeout) { return serverLoader({ "@/lib/db/queries": { getSystemSetting: async () => ({ value: { provider: "aoscdn", aosApiKey: "test-key" } }) },