保留框选工具栏与代理命令的既有计费规则 #48

Merged
WuYongJie merged 1 commits from codex/box-cutout-billing-paths-20261105 into main 2026-11-05 15:50:24 +08:00
3 changed files with 48 additions and 21 deletions

View File

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

View File

@ -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<string, any> = {
"@/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 },

View File

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