ai-chatbot/lib/canvas/capabilities/headless-executor.ts

472 lines
16 KiB
TypeScript

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";
import {
executeRmbgJob,
fetchImageBufferFromUrl,
} from "@/lib/ai/rmbg/service";
import type { CanvasCapabilityClientCommand } from "@/lib/canvas/capabilities/client-command";
import type { CanvasCapabilityTarget } from "@/lib/canvas/capabilities/context";
import { generateCanvasImage } from "@/lib/canvas/generate-image";
import { requestLongTaskResult } from "@/lib/client/long-task";
import {
findStyleProImageModelByRef,
getStyleProImageModelEntries,
sortStyleProImageModelEntries,
} from "@/lib/style-pro/model-options";
export type CanvasCapabilityMediaResult = {
assetId?: string;
height?: number;
metadata?: Record<string, unknown>;
nodeType?: "image" | "model3d" | "world3d";
prompt?: string;
sourceNodeId: string;
title: string;
url: string;
width?: number;
};
type HeadlessExecutorParams = {
command: CanvasCapabilityClientCommand;
requestHeaders: Headers;
requestUrl: string;
targets: CanvasCapabilityTarget[];
userId: string;
};
function asFiniteNumber(value: unknown) {
const number = Number(value);
return Number.isFinite(number) ? number : undefined;
}
function getCookieHeaders(headers: Headers) {
const cookie = headers.get("cookie") || "";
return cookie ? { cookie } : undefined;
}
async function postRuntimeEndpoint(params: {
body: Record<string, unknown>;
path: string;
requestHeaders: Headers;
requestUrl: string;
timeoutMs?: number;
}) {
const response = await fetch(new URL(params.path, params.requestUrl), {
method: "POST",
headers: {
"Content-Type": "application/json",
...getCookieHeaders(params.requestHeaders),
},
body: JSON.stringify(params.body),
signal: AbortSignal.timeout(params.timeoutMs || 20 * 60 * 1000),
});
const payload = await response.json().catch(() => null);
if (!response.ok || payload?.success === false || payload?.error) {
throw new Error(
String(payload?.error || payload?.detail || `能力服务返回 ${response.status}`)
);
}
return payload as Record<string, any>;
}
async function postRuntimeFormData(params: {
formData: FormData;
path: string;
requestHeaders: Headers;
requestUrl: string;
timeoutMs?: number;
}) {
const response = await fetch(new URL(params.path, params.requestUrl), {
method: "POST",
headers: getCookieHeaders(params.requestHeaders),
body: params.formData,
signal: AbortSignal.timeout(params.timeoutMs || 20 * 60 * 1000),
});
const payload = await response.json().catch(() => null);
if (!response.ok || payload?.success === false || payload?.error) {
throw new Error(
String(payload?.error || payload?.detail || `能力服务返回 ${response.status}`)
);
}
return payload as Record<string, any>;
}
function inferTripoImageType(imageUrl: string) {
try {
const extension = new URL(imageUrl).pathname.split(".").pop()?.toLowerCase();
if (extension === "png" || extension === "webp") return extension;
} catch {}
return "jpg";
}
async function resolveImageModel(modelRef?: unknown) {
const entries = sortStyleProImageModelEntries(
await getStyleProImageModelEntries({ includeHidden: false })
);
const requested = findStyleProImageModelByRef(entries, String(modelRef || ""));
const selected = requested || entries.find((entry) => entry.model.isDefault) || entries[0];
if (!selected) throw new Error("系统未配置可用的图片生成模型");
return selected;
}
async function executeForTarget(
params: HeadlessExecutorParams,
target: CanvasCapabilityTarget
): Promise<CanvasCapabilityMediaResult[]> {
const { command } = params;
const prompt = String(command.args.prompt || "").trim();
const common = {
sourceNodeId: target.nodeId,
prompt: prompt || undefined,
};
if (command.capabilityId === "image.remove_background") {
const imageBuffer = await fetchImageBufferFromUrl(target.sourceImageUrl);
const result = await executeRmbgJob({
userId: params.userId,
imageBuffer,
sourceImageUrl: target.sourceImageUrl,
prompt: prompt || "抠图处理",
skipGallerySave: false,
});
return [{
...common,
assetId: result.mediaAssetId || undefined,
title: `${target.title}-抠图`,
url: result.original_url,
}];
}
if (command.capabilityId === "image.extend") {
const result = await executeExtendJob({
userId: params.userId,
imageUrl: target.sourceImageUrl,
sourceImageUrl: target.sourceImageUrl,
prompt: prompt || "图像拓展处理",
xScale: asFiniteNumber(command.args.xScale),
yScale: asFiniteNumber(command.args.yScale),
requestId: command.requestId,
});
return [{
...common,
assetId: result.mediaAssetId || undefined,
title: `${target.title}-扩图`,
url: result.original_url,
}];
}
if (command.capabilityId === "image.resize") {
const width = asFiniteNumber(command.args.width);
const height = asFiniteNumber(command.args.height);
if (!width || !height) throw new Error("请提供完整的目标宽度和高度");
const result = await executeResizeJob({
userId: params.userId,
imageUrl: target.sourceImageUrl,
sourceImageUrl: target.sourceImageUrl,
canvasWidth: Math.round(width),
canvasHeight: Math.round(height),
backgroundColor: String(command.args.backgroundColor || "").trim() || undefined,
transparentBackground: command.args.transparentBackground === true,
mimeType: String(command.args.mimeType || "").trim() || undefined,
});
return [{
...common,
assetId: result.mediaAssetId || undefined,
height: result.canvas_height,
title: `${target.title}-${result.canvas_width}x${result.canvas_height}`,
url: result.original_url,
width: result.canvas_width,
}];
}
if (command.capabilityId === "image.box_cutout") {
const result = await executeBoxCutoutJob({
userId: params.userId,
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 || "框选抠图",
});
return [{
...common,
assetId: result.mediaAssetId || undefined,
metadata: {
cropRectApplied: result.cropRectApplied,
paddedCropRectApplied: result.paddedCropRectApplied,
},
title: `${target.title}-框选抠图`,
url: result.original_url,
}];
}
if (command.capabilityId === "image.layer_split") {
const result = await executeLayerSplitJob({
userId: params.userId,
imageUrl: target.sourceImageUrl,
sourceNodeId: target.nodeId,
prompt: prompt || undefined,
});
return result.layers.map((layer) => ({
...common,
assetId: layer.mediaAssetId || undefined,
metadata: {
bounds: layer.bounds || null,
layerIndex: layer.layerIndex,
model: result.model,
provider: result.provider,
totalLayers: layer.totalLayers,
},
title: layer.title || `${target.title}-图层${layer.layerIndex + 1}`,
url: layer.originalUrl,
}));
}
if (command.capabilityId === "image.enhance") {
const result = await requestLongTaskResult({
endpoint: new URL("/api/enhance", params.requestUrl).toString(),
payload: {
imageUrl: target.sourceImageUrl,
prompt: prompt || "图像增强处理",
scaleFactor: asFiniteNumber(command.args.scaleFactor),
},
pollingParams: {
prompt: prompt || "图像增强处理",
sourceImageUrl: target.sourceImageUrl,
},
timeoutMs: 10 * 60 * 1000,
headers: getCookieHeaders(params.requestHeaders),
});
if (!result?.success || !result.original_url) {
throw new Error(String(result?.error || result?.detail || "图像增强失败"));
}
return [{
...common,
title: `${target.title}-高清增强`,
url: String(result.original_url),
}];
}
if (command.capabilityId === "image.remove_watermark") {
const result = await requestLongTaskResult({
endpoint: new URL("/api/watermark-remove", params.requestUrl).toString(),
payload: { image_url: target.sourceImageUrl },
timeoutMs: 20 * 60 * 1000,
headers: getCookieHeaders(params.requestHeaders),
});
if (!result?.success || !result.original_url) {
throw new Error(String(result?.error || result?.detail || "去水印失败"));
}
return [{
...common,
title: `${target.title}-去水印`,
url: String(result.original_url),
}];
}
if (command.capabilityId === "image.multi_angle") {
const result = await postRuntimeEndpoint({
path: "/api/angle-edit",
requestUrl: params.requestUrl,
requestHeaders: params.requestHeaders,
body: {
image_url: target.sourceImageUrl,
horizontal_angle: asFiniteNumber(command.args.horizontalAngle) ?? 30,
vertical_angle: asFiniteNumber(command.args.verticalAngle) ?? 0,
zoom: asFiniteNumber(command.args.zoom) ?? 0,
},
});
const url = String(result.image_url || result.original_url || "").trim();
if (!url) throw new Error("多视角服务未返回图片");
return [{ ...common, title: `${target.title}-新视角`, url }];
}
if (command.capabilityId === "image.generate_3d_model") {
const modelVersion =
String(command.args.modelId || command.args.modelVersion || "").trim() ||
"tripo-standard";
const promptText = prompt || target.title || "图生 3D 模型";
const response = await postRuntimeEndpoint({
path: "/api/canvas/3d/tasks",
requestUrl: params.requestUrl,
requestHeaders: params.requestHeaders,
timeoutMs: 180_000,
body: {
taskType: "image_to_model",
imageUrl: target.sourceImageUrl,
imageType: inferTripoImageType(target.sourceImageUrl),
prompt: promptText,
modelVersion,
faceLimit: 120_000,
texture: true,
pbr: true,
autoSize: false,
quad: false,
generateParts: false,
smartLowPoly: false,
geometryQuality: "standard",
textureQuality: "standard",
textureAlignment: "original_image",
orientation: "default",
},
});
const task = response.data as Record<string, unknown> | undefined;
const taskId = String(task?.taskId || "").trim();
if (!taskId) throw new Error("3D 模型服务未返回任务 ID");
const providerId = String(task?.providerId || "").trim() || null;
const providerName = String(task?.providerName || "").trim() || null;
return [
{
...common,
nodeType: "model3d",
title: `${target.title}-3D模型`,
url: target.sourceImageUrl,
metadata: {
taskId,
provider: "tripo-3d",
mode: "image",
status: "generating",
sourceImageUrl: target.sourceImageUrl,
providerId,
providerName,
modelVersion,
generationStartedAt: new Date().toISOString(),
assetPayload: {
providerId,
providerName,
sourceImageUrl: target.sourceImageUrl,
},
},
},
];
}
if (command.capabilityId === "image.generate_3d_world") {
const modelVersion =
String(command.args.modelId || command.args.modelVersion || "").trim() ||
"world-standard";
const promptText = prompt || target.title || "图生 3D 世界";
const formData = new FormData();
formData.append("sourceUrl", target.sourceImageUrl);
formData.append("kind", "image");
const uploadResponse = await postRuntimeFormData({
formData,
path: "/api/canvas/world/upload",
requestUrl: params.requestUrl,
requestHeaders: params.requestHeaders,
timeoutMs: 180_000,
});
const upload = uploadResponse.data as Record<string, unknown> | undefined;
const mediaAssetId = String(upload?.mediaAssetId || "").trim();
if (!mediaAssetId) throw new Error("3D 世界服务未返回媒体资源 ID");
const providerId = String(upload?.providerId || "").trim() || null;
const providerName = String(upload?.providerName || "").trim() || null;
const taskResponse = await postRuntimeEndpoint({
path: "/api/canvas/world/tasks",
requestUrl: params.requestUrl,
requestHeaders: params.requestHeaders,
timeoutMs: 240_000,
body: {
mode: "image",
mediaAssetId,
providerId,
prompt: promptText,
displayName: target.title || promptText,
model: modelVersion,
disableRecaption: false,
makePublic: false,
},
});
const task = taskResponse.data as Record<string, unknown> | undefined;
const taskId = String(task?.taskId || "").trim();
if (!taskId) throw new Error("3D 世界服务未返回任务 ID");
const taskAssetPayload =
task?.assetPayload && typeof task.assetPayload === "object"
? (task.assetPayload as Record<string, unknown>)
: {};
return [
{
...common,
nodeType: "world3d",
title: `${target.title}-3D世界`,
url: target.sourceImageUrl,
metadata: {
taskId,
provider: "worldlabs-3d",
mode: "world-image",
status: "generating",
sourceImageUrl: target.sourceImageUrl,
providerId,
providerName,
modelVersion,
generationStartedAt: new Date().toISOString(),
assetPayload: {
...taskAssetPayload,
providerId,
providerName,
sourceImageUrl: target.sourceImageUrl,
},
},
},
];
}
if (
command.capabilityId === "image.prompt_regenerate" ||
command.capabilityId === "image.ai_wash"
) {
if (!prompt) throw new Error("请提供重绘提示词");
const model = await resolveImageModel(command.args.modelId);
const result = await generateCanvasImage({
prompt,
model: model.model.id,
ownerKey: params.userId,
imageCount: 1,
imageSize: String(command.args.imageSize || "2K"),
aspectRatio: String(command.args.aspectRatio || "auto"),
promptExtend: command.args.promptExtend === true,
referenceImage: target.sourceImageUrl,
debugLabel:
command.capabilityId === "image.ai_wash"
? "canvas-capability-ai-wash"
: "canvas-capability-regenerate",
debugMeta: {
capabilityId: command.capabilityId,
requestId: command.requestId,
sourceNodeId: target.nodeId,
},
});
const image = result.images[0];
if (!image?.originalUrl) throw new Error("图片模型未返回有效结果");
return [{
...common,
title:
command.capabilityId === "image.ai_wash"
? `${target.title}-AI洗图`
: `${target.title}-重绘`,
url: image.originalUrl,
}];
}
throw new Error(`尚未实现后台执行能力:${command.capabilityId}`);
}
export async function executeCanvasCapabilityHeadlessly(
params: HeadlessExecutorParams
) {
const results: CanvasCapabilityMediaResult[] = [];
for (const target of params.targets) {
results.push(...(await executeForTarget(params, target)));
}
return results;
}