fix: route canvas capabilities through local runtime

This commit is contained in:
134 2026-07-14 16:01:44 +08:00
parent 452effecbc
commit 1fd9be49c7
5 changed files with 101 additions and 5 deletions

View File

@ -10,6 +10,7 @@ import {
} from "@/lib/ai/rmbg/service";
import type { CanvasCapabilityClientCommand } from "@/lib/canvas/capabilities/client-command";
import type { CanvasCapabilityTarget } from "@/lib/canvas/capabilities/context";
import { resolveCanvasRuntimeEndpoint } from "@/lib/canvas/capabilities/internal-endpoint";
import { generateCanvasImage } from "@/lib/canvas/generate-image";
import { requestLongTaskResult } from "@/lib/client/long-task";
import {
@ -55,7 +56,7 @@ async function postRuntimeEndpoint(params: {
requestUrl: string;
timeoutMs?: number;
}) {
const response = await fetch(new URL(params.path, params.requestUrl), {
const response = await fetch(resolveCanvasRuntimeEndpoint(params.path, params.requestUrl), {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -80,7 +81,7 @@ async function postRuntimeFormData(params: {
requestUrl: string;
timeoutMs?: number;
}) {
const response = await fetch(new URL(params.path, params.requestUrl), {
const response = await fetch(resolveCanvasRuntimeEndpoint(params.path, params.requestUrl), {
method: "POST",
headers: getCookieHeaders(params.requestHeaders),
body: params.formData,
@ -233,7 +234,7 @@ async function executeForTarget(
if (command.capabilityId === "image.enhance") {
const result = await requestLongTaskResult({
endpoint: new URL("/api/enhance", params.requestUrl).toString(),
endpoint: resolveCanvasRuntimeEndpoint("/api/enhance", params.requestUrl),
payload: {
imageUrl: target.sourceImageUrl,
prompt: prompt || "图像增强处理",
@ -258,7 +259,10 @@ async function executeForTarget(
if (command.capabilityId === "image.remove_watermark") {
const result = await requestLongTaskResult({
endpoint: new URL("/api/watermark-remove", params.requestUrl).toString(),
endpoint: resolveCanvasRuntimeEndpoint(
"/api/watermark-remove",
params.requestUrl
),
payload: { image_url: target.sourceImageUrl },
timeoutMs: 20 * 60 * 1000,
headers: getCookieHeaders(params.requestHeaders),

View File

@ -0,0 +1,32 @@
type CanvasRuntimeEndpointOptions = {
internalOrigin?: string;
port?: string;
};
function normalizeInternalOrigin(value: string) {
const url = new URL(value);
url.pathname = "/";
url.search = "";
url.hash = "";
return url.toString();
}
export function resolveCanvasRuntimeEndpoint(
path: string,
requestUrl: string,
options: CanvasRuntimeEndpointOptions = {}
) {
const configuredOrigin = String(
options.internalOrigin ?? process.env.CANVAS_RUNTIME_INTERNAL_ORIGIN ?? ""
).trim();
if (configuredOrigin) {
return new URL(path, normalizeInternalOrigin(configuredOrigin)).toString();
}
const port = String(options.port ?? process.env.PORT ?? "").trim();
if (/^\d{1,5}$/.test(port) && Number(port) > 0 && Number(port) <= 65_535) {
return new URL(path, `http://127.0.0.1:${port}`).toString();
}
return new URL(path, requestUrl).toString();
}

View File

@ -20,7 +20,11 @@ export async function enqueueCanvasCapabilityExecution<T>(params: {
const state = getQueueState();
const previous = state.tails.get(params.projectId) || Promise.resolve();
const execution = previous.catch(() => undefined).then(params.execute);
const tail = execution.finally(() => {
const settledExecution = execution.then(
() => undefined,
() => undefined
);
const tail = settledExecution.finally(() => {
if (state.tails.get(params.projectId) === tail) {
state.tails.delete(params.projectId);
}

View File

@ -11,6 +11,7 @@ export default defineConfig({
/routes\/canvas-entry-performance\.test\.ts/,
/routes\/settings-visual-consistency\.test\.ts/,
/routes\/canvas-scale-adjust-intent\.test\.ts/,
/routes\/canvas-runtime-internal-endpoint\.test\.ts/,
/routes\/model-classification\.test\.ts/,
],
fullyParallel: true,

View File

@ -0,0 +1,55 @@
import { expect, test } from "@playwright/test";
import { resolveCanvasRuntimeEndpoint } from "@/lib/canvas/capabilities/internal-endpoint";
import {
enqueueCanvasCapabilityExecution,
getCanvasCapabilityQueueDepth,
} from "@/lib/canvas/capabilities/task-queue";
test.describe("canvas runtime internal endpoints", () => {
test("uses the local HTTP listener for server-internal capability calls", () => {
expect(
resolveCanvasRuntimeEndpoint(
"/api/enhance",
"https://ai.yydd.ai/api/canvas/canvas-1/capabilities",
{ port: "3125" }
)
).toBe("http://127.0.0.1:3125/api/enhance");
expect(
resolveCanvasRuntimeEndpoint(
"/api/enhance",
"https://ai.yydd.ai/api/canvas/canvas-1/capabilities",
{ internalOrigin: "http://canvas-runtime:3000/base" }
)
).toBe("http://canvas-runtime:3000/api/enhance");
});
test("falls back to the incoming origin without an internal listener", () => {
expect(
resolveCanvasRuntimeEndpoint(
"/api/enhance",
"https://ai.yydd.ai/api/canvas/canvas-1/capabilities",
{ port: "invalid" }
)
).toBe("https://ai.yydd.ai/api/enhance");
});
test("continues the project queue after a failed capability execution", async () => {
await expect(
enqueueCanvasCapabilityExecution({
projectId: "failed-capability-project",
execute: async () => {
throw new Error("expected capability failure");
},
})
).rejects.toThrow("expected capability failure");
await expect(
enqueueCanvasCapabilityExecution({
projectId: "failed-capability-project",
execute: async () => "recovered",
})
).resolves.toBe("recovered");
expect(getCanvasCapabilityQueueDepth("failed-capability-project")).toBe(0);
});
});