diff --git a/app/api/angle-edit/route.ts b/app/api/angle-edit/route.ts
index 403c2a204..6a4ab34b0 100644
--- a/app/api/angle-edit/route.ts
+++ b/app/api/angle-edit/route.ts
@@ -1,385 +1,10 @@
-import { NextRequest, NextResponse } from "next/server";
-import {
- createMediaAssetRelation,
- getMediaAssetsByUserAndUrls,
- getPointsSystemConfig,
- getSystemSetting,
- upsertMediaAsset,
-} from "@/lib/db/queries";
+import { NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
-import { uploadFile } from "@/lib/storage/service";
-import { db } from "@/lib/db";
-import { generatedImage } from "@/lib/db/schema";
-import { createLogger } from "@/lib/observability/logger";
-import { getImageToolCreditCost } from "@/lib/points/config";
-import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
-
-const logger = createLogger("angle-edit-api");
-
-// 角度编辑API配置类型
-type AngleEditConfig = {
- enabled?: boolean;
- falApiKey?: string;
- loraScale?: number;
- inferenceSteps?: number;
- guidanceScale?: number;
- maxImageSize?: number;
- timeout?: number;
-};
-
-// 设置API路由最大执行时间为3分钟(180秒)
+import { executeAngleEditPOST } from "@/lib/canvas/operations/angle-edit";
+export const runtime = "nodejs";
export const maxDuration = 180;
-
-/**
- * 压缩图片生成缩略图
- */
-async function createThumbnail(buffer: Buffer): Promise {
- try {
- const sharp = await import("sharp");
-
- const thumbnailBuffer = await sharp.default(buffer)
- .resize(500, null, {
- fit: "inside",
- withoutEnlargement: true,
- })
- .jpeg({ quality: 80 })
- .toBuffer();
-
- logger.debug("thumbnail_created", {
- originalBytes: buffer.length,
- thumbnailBytes: thumbnailBuffer.length,
- });
- return thumbnailBuffer;
- } catch (error) {
- logger.warn("thumbnail_creation_failed", error);
- return buffer;
- }
-}
-
-/**
- * 将 base64 图片上传到 S3
- */
-async function uploadBase64ToS3(base64Data: string, prefix: string = "angle-edit"): Promise<{ thumbnail: string; original: string }> {
- try {
- const buffer = Buffer.from(base64Data, "base64");
- const timestamp = Date.now();
- const randomStr = Math.random().toString(36).slice(2, 8);
- const thumbnailFilename = `${prefix}-thumb-${timestamp}-${randomStr}.png`;
- const originalFilename = `${prefix}-orig-${timestamp}-${randomStr}.png`;
-
- logger.debug("source_image_prepared", { originalBytes: buffer.length });
-
- const thumbnailBuffer = await createThumbnail(buffer);
-
- const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice(
- thumbnailBuffer.byteOffset,
- thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength
- ) as ArrayBuffer;
- const originalArrayBuffer = buffer.buffer.slice(
- buffer.byteOffset,
- buffer.byteOffset + buffer.byteLength
- ) as ArrayBuffer;
-
- logger.debug("storage_upload_started", {
- hasOriginal: originalArrayBuffer.byteLength > 0,
- hasThumbnail: thumbnailArrayBuffer.byteLength > 0,
- prefix,
- });
- const [thumbnailResult, originalResult] = await Promise.all([
- uploadFile(thumbnailFilename, thumbnailArrayBuffer),
- uploadFile(originalFilename, originalArrayBuffer),
- ]);
-
- logger.info("storage_upload_completed", {
- originalUrl: originalResult.url,
- thumbnailUrl: thumbnailResult.url,
- });
-
- return {
- thumbnail: thumbnailResult.url,
- original: originalResult.url,
- };
- } catch (error) {
- logger.error("storage_upload_failed", error);
- throw new Error(`图片上传失败: ${error instanceof Error ? error.message : "未知错误"}`);
- }
-}
-
-export async function POST(request: NextRequest) {
- const startTime = Date.now();
-
- try {
- // 1. 验证用户登录
- const session = await auth();
- if (!session?.user?.id) {
- return NextResponse.json({ error: "未登录" }, { status: 401 });
- }
-
- logger.info("request_received", { authenticated: true });
-
- // 2. 从数据库获取配置
- const angleEditConfig = await getSystemSetting({ key: "angle_edit_config" });
- const config = (angleEditConfig?.value as AngleEditConfig) || {};
-
- // 验证功能是否启用
- if (!config.enabled) {
- return NextResponse.json(
- { error: "角度编辑功能未启用,请联系管理员在系统设置中开启" },
- { status: 400 }
- );
- }
-
- // 验证 API Key
- if (!config.falApiKey) {
- return NextResponse.json(
- { error: "fal.ai API Key 未配置,请联系管理员配置" },
- { status: 500 }
- );
- }
-
- logger.debug("config_loaded", {
- enabled: Boolean(config.enabled),
- hasApiKey: Boolean(config.falApiKey),
- loraScale: config.loraScale || 1,
- });
-
- // 3. 解析请求
- const body = await request.json();
- const {
- image_url,
- image_base64,
- // 数值参数 (官方 API 格式)
- horizontal_angle, // 0-360 度
- vertical_angle, // -30 到 90 度
- zoom, // 0-10
- lora_scale,
- } = body;
-
- if (!image_url && !image_base64) {
- return NextResponse.json({ error: "缺少图片数据" }, { status: 400 });
- }
-
- // 使用请求中的参数或配置中的默认值
- const finalLoraScale = lora_scale ?? config.loraScale ?? 1;
- const finalHorizontalAngle = horizontal_angle ?? 0;
- const finalVerticalAngle = vertical_angle ?? 0;
- const finalZoom = zoom ?? 5;
-
- logger.info("request_parameters_normalized", {
- hasBase64Input: Boolean(image_base64),
- hasUrlInput: Boolean(image_url),
- horizontalAngle: finalHorizontalAngle,
- loraScale: finalLoraScale,
- verticalAngle: finalVerticalAngle,
- zoom: finalZoom,
- });
-
- const pointsConfig = await getPointsSystemConfig();
- const pointsCharge = await prepareFeaturePointsCharge({
- userId: session.user.id,
- unitCost: getImageToolCreditCost(pointsConfig, "angle-edit"),
- reason: "image_angle_edit",
- modelId: "qwen-image-edit-2511-multiple-angles",
- note: "多角度编辑消耗积分",
- pointsConfig,
- });
- if (!pointsCharge.ok) {
- return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status });
- }
-
- // 4. 准备图片输入
- let imageInput: string;
- if (image_url) {
- imageInput = image_url;
- } else {
- const mimeType = image_base64.startsWith("/9j/") ? "image/jpeg" : "image/png";
- imageInput = `data:${mimeType};base64,${image_base64}`;
- }
-
- // 5. 动态导入 fal.ai SDK 并调用官方多角度 API
- logger.info("provider_call_started", {
- modelId: "fal-ai/qwen-image-edit-2511-multiple-angles",
- });
-
- const fal = await import("@fal-ai/serverless-client");
-
- fal.config({
- credentials: config.falApiKey,
- });
-
- // 使用官方多角度 API 端点
- // 文档: https://fal.ai/models/fal-ai/qwen-image-edit-2511-multiple-angles
- const result = await fal.subscribe("fal-ai/qwen-image-edit-2511-multiple-angles", {
- input: {
- image_urls: [imageInput], // 官方使用 image_urls (数组)
- horizontal_angle: finalHorizontalAngle, // 水平旋转角度 0-360
- vertical_angle: finalVerticalAngle, // 垂直角度 -30 到 90
- zoom: finalZoom, // 缩放 0-10
- lora_scale: finalLoraScale, // LoRA 强度
- num_inference_steps: config.inferenceSteps || 28,
- guidance_scale: config.guidanceScale || 4.5, // 官方默认 4.5
- enable_safety_checker: true,
- output_format: "png",
- },
- logs: true,
- onQueueUpdate: (update) => {
- if (update.status === "IN_PROGRESS") {
- logger.debug("provider_progress", { status: update.status });
- }
- },
- });
-
- // 6. 处理结果
- const apiResult = result as { images?: Array<{ url?: string }>; prompt?: string };
- const outputImage = apiResult.images?.[0];
- if (!outputImage?.url) {
- throw new Error("API 未返回有效图片");
- }
-
- const outputImageUrl = outputImage.url;
- logger.info("provider_call_completed", {
- hasGeneratedPrompt: Boolean(apiResult.prompt),
- imageCount: apiResult.images?.length ?? 0,
- outputImageUrl,
- });
-
- // 7. 下载图片并上传到我们的 S3
- const imageResponse = await fetch(outputImageUrl);
- if (!imageResponse.ok) {
- throw new Error(`下载输出图片失败: ${imageResponse.status}`);
- }
- const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
- const imageBase64Result = imageBuffer.toString("base64");
-
- const s3Result = await uploadBase64ToS3(imageBase64Result, "angle-edit");
- logger.info("result_image_persisted", {
- originalUrl: s3Result.original,
- thumbnailUrl: s3Result.thumbnail,
- });
-
- // 8. 保存到数据库
- const processingTime = (Date.now() - startTime) / 1000;
-
- const [savedImage] = await db
- .insert(generatedImage)
- .values({
- userId: session.user.id,
- thumbnailUrl: s3Result.thumbnail,
- originalUrl: s3Result.original,
- prompt: `角度编辑: H${finalHorizontalAngle}° V${finalVerticalAngle}° Z${finalZoom}`,
- modelId: "qwen-image-edit-2511-multiple-angles",
- createdAt: new Date(),
- })
- .returning();
-
- if (pointsCharge.chargeable) {
- try {
- await pointsCharge.consume({
- note: `多角度编辑消耗积分(${savedImage.id})`,
- });
- } catch (pointsError) {
- logger.error("points_consume_failed", pointsError, {
- imageId: savedImage.id,
- userId: session.user.id,
- });
- }
- }
-
- try {
- const sourceAssets =
- typeof image_url === "string" && image_url.trim()
- ? await getMediaAssetsByUserAndUrls({
- userId: session.user.id,
- urls: [image_url],
- })
- : [];
- const media = await upsertMediaAsset({
- userId: session.user.id,
- mediaType: "image",
- sourceType: "edited",
- status: "ready",
- originalUrl: s3Result.original,
- previewUrl: s3Result.original,
- thumbnailUrl: s3Result.thumbnail,
- displayName: `角度编辑 ${finalHorizontalAngle}/${finalVerticalAngle}`.slice(0, 80),
- prompt: `角度编辑: H${finalHorizontalAngle}° V${finalVerticalAngle}° Z${finalZoom}`,
- modelId: "qwen-image-edit-2511-multiple-angles",
- sourceTable: "GeneratedImage",
- sourceRowId: savedImage.id,
- primaryParentAssetId: sourceAssets[0]?.id ?? null,
- metadata: {
- horizontalAngle: finalHorizontalAngle,
- verticalAngle: finalVerticalAngle,
- zoom: finalZoom,
- },
- });
- if (media) {
- for (const sourceAsset of sourceAssets) {
- await createMediaAssetRelation({
- userId: session.user.id,
- fromAssetId: sourceAsset.id,
- toAssetId: media.id,
- relationType: "edited_from",
- metadata: {
- horizontalAngle: finalHorizontalAngle,
- verticalAngle: finalVerticalAngle,
- zoom: finalZoom,
- },
- });
- }
- }
- } catch (assetError) {
- logger.warn("media_asset_registration_failed", assetError);
- }
-
- logger.info("request_completed", {
- processingSeconds: Number(processingTime.toFixed(2)),
- });
-
- // 9. 返回结果
- return NextResponse.json({
- success: true,
- image_url: s3Result.original,
- thumbnail_url: s3Result.thumbnail,
- image_base64: imageBase64Result,
- processing_time: processingTime,
- angles: {
- horizontal_angle: finalHorizontalAngle,
- vertical_angle: finalVerticalAngle,
- zoom: finalZoom,
- },
- prompt: apiResult.prompt,
- });
-
- } catch (error) {
- logger.error("request_failed", error);
-
- // 输出详细的错误信息
- if (error && typeof error === "object" && "status" in error) {
- const apiError = error as { status?: number; body?: unknown; message?: string };
- logger.warn("provider_error_summary", {
- bodyType: typeof apiError.body,
- hasBody: apiError.body != null,
- message: apiError.message,
- status: apiError.status,
- });
-
- if (apiError.status === 403) {
- return NextResponse.json(
- {
- error: "API 访问被拒绝 (403 Forbidden)。请检查 API Key 是否正确,以及是否有权限访问该模型。",
- details: apiError.body,
- },
- { status: 403 }
- );
- }
- }
-
- return NextResponse.json(
- {
- error: error instanceof Error ? error.message : "角度编辑处理失败",
- },
- { status: 500 }
- );
- }
+export async function POST(request: Request) {
+ const session = await auth();
+ if (!session?.user?.id) return NextResponse.json({ error: "未登录" }, { status: 401 });
+ return executeAngleEditPOST(request, session.user.id);
}
diff --git a/app/api/canvas/generation-tasks/route.ts b/app/api/canvas/generation-tasks/route.ts
index 3c7181aa2..064461475 100644
--- a/app/api/canvas/generation-tasks/route.ts
+++ b/app/api/canvas/generation-tasks/route.ts
@@ -1,3 +1,4 @@
+import { imageOperationSchema } from "@/lib/video-canvas/image-operation";
import { after, NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
import { z } from "zod";
@@ -39,6 +40,17 @@ export async function POST(request: Request) {
throw Object.assign(Error("任务数据过大,请使用已上传的素材链接"), { status: 413 });
const parsed = canvasTaskRequestSchema.safeParse(JSON.parse(text));
if (!parsed.success) throw Object.assign(Error("任务参数不合法"), { status: 400 });
+ if (
+ parsed.data.kind === "image-operation" &&
+ !imageOperationSchema.safeParse(parsed.data.params).success
+ )
+ throw Object.assign(Error("图片处理参数不合法"), { status: 400 });
+ if (
+ parsed.data.attachment &&
+ ((parsed.data.attachment.type === "story-asset" && parsed.data.kind !== "image") ||
+ (parsed.data.attachment.type === "story-prompts" && parsed.data.kind !== "analysis"))
+ )
+ throw Object.assign(Error("脚本结果类型不匹配"), { status: 400 });
const userId = await owner(parsed.data.projectId);
const task = await createCanvasGenerationTask(userId, parsed.data);
kick(userId, parsed.data.projectId);
diff --git a/app/api/enhance/route.ts b/app/api/enhance/route.ts
index ea888b293..bba47dd54 100644
--- a/app/api/enhance/route.ts
+++ b/app/api/enhance/route.ts
@@ -1,1411 +1,15 @@
-import { NextRequest, NextResponse } from "next/server";
-import {
- createMediaAssetRelation,
- getAiProvidersByType,
- getMediaAssetsByUserAndUrls,
- getSystemSetting,
- upsertMediaAsset,
-} from "@/lib/db/queries";
+import { NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
-import { uploadFile } from "@/lib/storage/service";
-import { db } from "@/lib/db";
-import { generatedImage } from "@/lib/db/schema";
-import { executeConfiguredVip98kTask } from "@/lib/ai/vip98k-runtime";
-import { createFalClient } from "@fal-ai/client";
-
-// 变清晰API配置类型
-type EnhanceApiConfig = {
- provider?: "vip98k" | "aoscdn";
- apiUrl?: string;
- apiKey?: string;
- type?: string; // "clean" | "face"
- scaleFactor?: number | string; // 1 | 2 | 4 | "auto"
- // 备用API配置 (Real-ESRGAN)
- useBackupApi?: boolean;
- backupApiUrl?: string;
- backupApiKey?: string;
- backupScale?: string; // "2" | "4" | "8"
- backupModel?: string;
-};
-
-const AOS_MAX_RESOLUTION = 4096;
-const ENHANCE_CREATE_TIMEOUT_MS = 120_000;
-const ENHANCE_CREATE_RETRY_COUNT = 1;
-const ENHANCE_SYNC_WAIT_TIMEOUT_MS = 20_000;
-const ENHANCE_TASK_POLL_INTERVAL_MS = 1200;
-
-function getAosFriendlyErrorMessage(detail: string): string | null {
- const resolutionMatch = detail.match(/resolution excess,\s*width=(\d+),\s*height=(\d+)/i);
- if (resolutionMatch) {
- const [, width, height] = resolutionMatch;
- return `图片分辨率超限(${width}x${height}),AOS要求宽高都不超过 ${AOS_MAX_RESOLUTION}px。请缩小后重试。`;
- }
-
- if (/invalid file/i.test(detail)) {
- return "图片文件无法识别,请尝试转换为 JPG/PNG 后重试。";
- }
-
- return null;
-}
-
-function isRetryableNetworkError(error: unknown): boolean {
- if (!(error instanceof Error)) return false;
- if (error.name === "AbortError") return true;
-
- const message = error.message || "";
- const cause = (error as Error & { cause?: { message?: string; code?: string } }).cause;
- const causeMessage = cause?.message || "";
- const causeCode = cause?.code || "";
- const combined = `${message} ${causeMessage} ${causeCode}`;
-
- return /EHOSTUNREACH|ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT|No route to host/i.test(combined);
-}
-
-async function ensureAosResolutionLimit(
- buffer: Buffer,
- logPrefix: string
-): Promise {
- try {
- const sharp = await import("sharp");
- const image = sharp.default(buffer);
- const metadata = await image.metadata();
- const width = metadata.width;
- const height = metadata.height;
-
- if (!width || !height) {
- return buffer;
- }
-
- if (width <= AOS_MAX_RESOLUTION && height <= AOS_MAX_RESOLUTION) {
- return buffer;
- }
-
- const resizedBuffer = await image
- .resize(AOS_MAX_RESOLUTION, AOS_MAX_RESOLUTION, {
- fit: "inside",
- withoutEnlargement: true,
- })
- .toBuffer();
- const resizedMeta = await sharp.default(resizedBuffer).metadata();
-
- console.warn(
- `[${logPrefix}] 输入图片分辨率超限,已自动缩放: ${width}x${height} -> ${resizedMeta.width}x${resizedMeta.height}`
- );
- return resizedBuffer;
- } catch (error) {
- console.error(`[${logPrefix}] 自动缩放失败,继续使用原图:`, error);
- return buffer;
- }
-}
-
-/**
- * 压缩图片生成缩略图(使用 sharp)
- */
-async function createThumbnail(buffer: Buffer, mimeType: string): Promise {
- try {
- const sharp = await import("sharp");
-
- // 创建缩略图(宽度 500px,高度按比例,质量 80%)
- const thumbnailBuffer = await sharp.default(buffer)
- .resize(500, null, {
- fit: "inside",
- withoutEnlargement: true,
- })
- .jpeg({ quality: 80 }) // JPEG格式
- .toBuffer();
-
- console.log(`[Enhance] Thumbnail created, size: ${thumbnailBuffer.length} bytes (original: ${buffer.length} bytes)`);
- return thumbnailBuffer;
- } catch (error) {
- console.error("[Enhance] Failed to create thumbnail:", error);
- // 如果失败,返回原图
- return buffer;
- }
-}
-
-/**
- * 将 base64 图片上传到 S3,返回缩略图和原图的 URL
- */
-async function uploadBase64ToS3(base64Data: string, mimeType: string = "image/jpeg"): Promise<{ thumbnail: string; original: string }> {
- try {
- // 解析 base64 数据
- const buffer = Buffer.from(base64Data, "base64");
-
- // 生成文件名
- const ext = mimeType.split("/")[1] || "jpg";
- const timestamp = Date.now();
- const randomStr = Math.random().toString(36).slice(2, 8);
- const thumbnailFilename = `enhance-thumb-${timestamp}-${randomStr}.${ext}`;
- const originalFilename = `enhance-orig-${timestamp}-${randomStr}.${ext}`;
-
- console.log(`[Enhance] Original size: ${buffer.length} bytes`);
-
- // 生成缩略图
- const thumbnailBuffer = await createThumbnail(buffer, mimeType);
-
- // 转换为 ArrayBuffer
- const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice(
- thumbnailBuffer.byteOffset,
- thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength
- ) as ArrayBuffer;
- const originalArrayBuffer = buffer.buffer.slice(
- buffer.byteOffset,
- buffer.byteOffset + buffer.byteLength
- ) as ArrayBuffer;
-
- // 并行上传缩略图和原图到 S3
- console.log(`[Enhance] Uploading thumbnail and original to S3...`);
- const [thumbnailResult, originalResult] = await Promise.all([
- uploadFile(thumbnailFilename, thumbnailArrayBuffer),
- uploadFile(originalFilename, originalArrayBuffer),
- ]);
-
- console.log(`[Enhance] Uploaded to S3:`);
- console.log(` - Thumbnail: ${thumbnailResult.url}`);
- console.log(` - Original: ${originalResult.url}`);
-
- return {
- thumbnail: thumbnailResult.url,
- original: originalResult.url,
- };
- } catch (error) {
- console.error("[Enhance] Failed to upload to S3:", error);
- throw new Error(`图片上传到存储桶失败: ${error instanceof Error ? error.message : "未知错误"}。请检查系统设置中的存储配置。`);
- }
-}
-
-// 设置API路由最大执行时间为3分钟(180秒)
+import { executeEnhanceGET, executeEnhancePOST } from "@/lib/canvas/operations/enhance";
+export const runtime = "nodejs";
export const maxDuration = 180;
-
-/**
- * 使用Real-ESRGAN备用API处理图片(通过URL方式)
- * @param imageUrl 图片URL(需要公开可访问)
- * @param config 备用API配置
- * @returns 处理后的图片Base64和S3 URL
- */
-async function processWithRealESRGAN(
- imageUrl: string,
- config: {
- backupApiUrl: string;
- backupApiKey: string;
- backupScale: string;
- backupModel: string;
- }
-): Promise<{ imageBase64: string; imageUrl: string; processingTime: number }> {
- const startTime = Date.now();
- const apiUrl = config.backupApiUrl.endsWith("/")
- ? config.backupApiUrl.slice(0, -1)
- : config.backupApiUrl;
-
- const upscaleUrl = `${apiUrl}/upscale/url`;
- console.log(`[Enhance-RealESRGAN] 调用URL方式API: ${upscaleUrl}`);
- console.log(`[Enhance-RealESRGAN] 图片URL: ${imageUrl}`);
- console.log(`[Enhance-RealESRGAN] 放大倍数: ${config.backupScale}x, 模型: ${config.backupModel}`);
-
- const response = await fetch(upscaleUrl, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "X-API-Key": config.backupApiKey,
- },
- body: JSON.stringify({
- image_url: imageUrl,
- scale: Number.parseInt(config.backupScale) || 4,
- model_name: config.backupModel || "RealESRGAN_x4plus",
- }),
- });
-
- if (!response.ok) {
- const errorText = await response.text();
- console.error(`[Enhance-RealESRGAN] API请求失败: ${response.status} ${response.statusText}`);
- console.error(`[Enhance-RealESRGAN] 错误详情: ${errorText}`);
- throw new Error(`Real-ESRGAN API请求失败: ${response.status} - ${errorText || response.statusText}`);
- }
-
- const result = await response.json();
- console.log(`[Enhance-RealESRGAN] API响应:`, {
- success: result.success,
- hasImageUrl: !!result.image_url,
- processingTime: result.processing_time,
- totalTime: result.total_time,
- });
-
- if (!result.success || !result.image_url) {
- throw new Error(`Real-ESRGAN处理失败: ${JSON.stringify(result)}`);
- }
-
- // Real-ESRGAN API直接返回S3 URL,我们需要下载图片并转换为Base64
- console.log(`[Enhance-RealESRGAN] 下载处理后的图片: ${result.image_url}`);
- const imageResponse = await fetch(result.image_url);
- if (!imageResponse.ok) {
- throw new Error(`无法下载Real-ESRGAN处理后的图片: ${imageResponse.status}`);
- }
-
- const imageBuffer = await imageResponse.arrayBuffer();
- const imageBase64 = Buffer.from(imageBuffer).toString("base64");
-
- const processingTime = (Date.now() - startTime) / 1000;
- console.log(`[Enhance-RealESRGAN] ✅ 处理完成,总耗时: ${processingTime.toFixed(2)}秒`);
- console.log(`[Enhance-RealESRGAN] ✅ 处理后图片大小: ${imageBuffer.byteLength} bytes`);
-
- return {
- imageBase64,
- imageUrl: result.image_url,
- processingTime,
- };
+export async function GET(request: Request) {
+ const session = await auth();
+ if (!session?.user?.id) return NextResponse.json({ error: "未登录" }, { status: 401 });
+ return executeEnhanceGET(request, session.user.id);
}
-
-/**
- * 使用Real-ESRGAN备用API处理图片(通过Base64方式,用于没有公开URL的图片)
- * @param imageBase64 图片Base64数据
- * @param config 备用API配置
- * @returns 处理后的图片Base64
- */
-async function processWithRealESRGANBase64(
- imageBase64: string,
- config: {
- backupApiUrl: string;
- backupApiKey: string;
- backupScale: string;
- backupModel: string;
- }
-): Promise<{ imageBase64: string; processingTime: number }> {
- const startTime = Date.now();
- const apiUrl = config.backupApiUrl.endsWith("/")
- ? config.backupApiUrl.slice(0, -1)
- : config.backupApiUrl;
-
- const upscaleUrl = `${apiUrl}/upscale/base64`;
- console.log(`[Enhance-RealESRGAN] 调用Base64方式API: ${upscaleUrl}`);
- console.log(`[Enhance-RealESRGAN] 放大倍数: ${config.backupScale}x, 模型: ${config.backupModel}`);
- console.log(`[Enhance-RealESRGAN] 输入图片Base64长度: ${imageBase64.length}`);
-
- const response = await fetch(upscaleUrl, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "X-API-Key": config.backupApiKey,
- },
- body: JSON.stringify({
- image_base64: imageBase64,
- scale: Number.parseInt(config.backupScale) || 4,
- model_name: config.backupModel || "RealESRGAN_x4plus",
- }),
- });
-
- if (!response.ok) {
- const errorText = await response.text();
- console.error(`[Enhance-RealESRGAN] API请求失败: ${response.status} ${response.statusText}`);
- console.error(`[Enhance-RealESRGAN] 错误详情: ${errorText}`);
- throw new Error(`Real-ESRGAN API请求失败: ${response.status} - ${errorText || response.statusText}`);
- }
-
- const result = await response.json();
- console.log(`[Enhance-RealESRGAN] API响应:`, {
- success: result.success,
- hasImageBase64: !!result.image_base64,
- processingTime: result.processing_time,
- });
-
- if (!result.success || !result.image_base64) {
- throw new Error(`Real-ESRGAN处理失败: ${JSON.stringify(result)}`);
- }
-
- const processingTime = (Date.now() - startTime) / 1000;
- console.log(`[Enhance-RealESRGAN] ✅ 处理完成,总耗时: ${processingTime.toFixed(2)}秒`);
- console.log(`[Enhance-RealESRGAN] ✅ 处理后图片Base64长度: ${result.image_base64.length}`);
-
- return {
- imageBase64: result.image_base64,
- processingTime,
- };
-}
-
-async function processWithFalEsrgan(
- imageBase64: string,
- requestedScaleFactor: number,
-): Promise<{ imageBase64: string; processingTime: number }> {
- const providers = await getAiProvidersByType({
- type: "fal-image",
- enabledOnly: true,
- });
- const provider = providers.find((item) => item.apiKey?.trim());
- if (!provider?.apiKey) {
- throw new Error("未配置可用的 FAL 专用高清服务");
- }
-
- const scale = Math.max(2, Math.min(8, requestedScaleFactor || 4));
- const model = scale <= 2 ? "RealESRGAN_x2plus" : "RealESRGAN_x4plus";
- const credentials = provider.apiKey
- .trim()
- .replace(/^(?:Key|Bearer)\s+/i, "");
- const fal = createFalClient({ credentials });
- const startedAt = Date.now();
- const result: any = await fal.subscribe("fal-ai/esrgan", {
- input: {
- image_url: `data:image/jpeg;base64,${imageBase64}`,
- scale,
- model,
- output_format: "png",
- },
- logs: true,
- timeout: 180_000,
- });
- const outputUrl = result?.data?.image?.url || result?.image?.url;
- if (!outputUrl) {
- throw new Error("FAL ESRGAN 专用高清服务未返回图片");
- }
-
- const outputResponse = await fetch(outputUrl, {
- cache: "no-store",
- signal: AbortSignal.timeout(120_000),
- });
- if (!outputResponse.ok) {
- throw new Error(
- `FAL ESRGAN 高清结果下载失败(HTTP ${outputResponse.status})`,
- );
- }
-
- return {
- imageBase64: Buffer.from(await outputResponse.arrayBuffer()).toString(
- "base64",
- ),
- processingTime: (Date.now() - startedAt) / 1000,
- };
-}
-
-async function processWithVip98kEnhance(params: {
- userId: string;
- imageBase64: string;
- prompt: string;
- chatId: string | null;
- messageId: string | null;
- sourceImageUrl: string | null;
- scaleFactor: number;
-}) {
- const startedAt = Date.now();
- const result = await executeConfiguredVip98kTask({
- modelId: "enhance",
- kind: "image-tool",
- prompt: `${params.prompt || "Enhance image clarity"}. Upscale ${params.scaleFactor}x while preserving composition, color and texture.`,
- imageUrls: [`data:image/jpeg;base64,${params.imageBase64}`],
- imageParams: { scaleFactor: params.scaleFactor },
- });
- const outputUrl = result.outputUrls[0];
- if (!outputUrl) throw new Error("VIP98K 变清晰未返回图片");
-
- const response = await fetch(outputUrl, {
- cache: "no-store",
- signal: AbortSignal.timeout(120_000),
- });
- if (!response.ok) {
- throw new Error(`VIP98K 变清晰结果下载失败(HTTP ${response.status})`);
- }
- const outputBuffer = Buffer.from(await response.arrayBuffer());
- const outputBase64 = outputBuffer.toString("base64");
- const uploaded = await uploadBase64ToS3(outputBase64, "image/png");
- const [savedImage] = await db
- .insert(generatedImage)
- .values({
- userId: params.userId,
- chatId: params.chatId,
- messageId: params.messageId,
- thumbnailUrl: uploaded.thumbnail,
- originalUrl: uploaded.original,
- imageSize: `${params.scaleFactor}x放大`,
- prompt: params.prompt,
- modelId: "enhance",
- })
- .returning();
-
- try {
- const sourceAssets = params.sourceImageUrl
- ? await getMediaAssetsByUserAndUrls({
- userId: params.userId,
- urls: [params.sourceImageUrl],
- })
- : [];
- const media = await upsertMediaAsset({
- userId: params.userId,
- chatId: params.chatId,
- mediaType: "image",
- sourceType: "edited",
- status: "ready",
- originalUrl: uploaded.original,
- previewUrl: uploaded.original,
- thumbnailUrl: uploaded.thumbnail,
- displayName: params.prompt.slice(0, 80),
- prompt: params.prompt,
- modelId: "enhance",
- sourceMessageId: params.messageId,
- sourceTable: "GeneratedImage",
- sourceRowId: savedImage?.id || null,
- primaryParentAssetId: sourceAssets[0]?.id ?? null,
- metadata: { provider: "vip98k", scaleFactor: params.scaleFactor },
- });
- if (media) {
- for (const sourceAsset of sourceAssets) {
- await createMediaAssetRelation({
- userId: params.userId,
- chatId: params.chatId,
- fromAssetId: sourceAsset.id,
- toAssetId: media.id,
- relationType: "edited_from",
- sourceMessageId: params.messageId,
- metadata: { prompt: params.prompt, provider: "vip98k" },
- });
- }
- }
- } catch (error) {
- console.warn("[Enhance-VIP98K] MediaAsset registration failed", error);
- }
-
- return {
- success: true,
- image_base64: outputBase64,
- thumbnail_url: uploaded.thumbnail,
- original_url: uploaded.original,
- processing_time: (Date.now() - startedAt) / 1000,
- api_type: "vip98k",
- };
-}
-
-/**
- * 轮询获取任务结果(根据官方示例)
- */
-async function pollTaskResult(
- apiUrl: string,
- apiKey: string,
- taskId: string,
- delay: number = 1000,
- timeout: number = 120000 // 增加到120秒(2分钟)
-): Promise {
- const pollUrl = `${apiUrl}/api/tasks/visual/scale/${taskId}`;
-
- const getTaskResult = async (): Promise => {
- const response = await fetch(pollUrl, {
- method: "GET",
- headers: {
- "X-API-KEY": apiKey,
- },
- });
-
- if (!response.ok) {
- throw new Error(`轮询请求失败: ${response.status} ${response.statusText}`);
- }
-
- const result = await response.json();
-
- if (!result.data) {
- throw result;
- }
-
- const { progress, state } = result.data;
- console.log(`[Enhance] 轮询进度: progress=${progress}%, state=${state}, task_id=${taskId}`);
-
- // state < 0 表示失败
- if (state < 0) {
- const errMessage = String(
- result.data?.err_message || result.data?.err_info || result.data?.state_detail || `state=${state}`
- );
- throw new Error(`AOS任务失败: ${errMessage}`);
- }
-
- // progress >= 100 表示完成
- if (progress >= 100) {
- console.log(`[Enhance] 任务完成,task_id=${taskId}`);
- return result;
- }
-
- // 否则继续轮询
- throw null;
- };
-
- try {
- const result = await getTaskResult();
- return result;
- } catch (error) {
- if (error && typeof error === "object" && "data" in error) {
- throw new Error(JSON.stringify(error, null, 2));
- }
- if (timeout <= 0) {
- throw new Error("轮询超时");
- }
- await new Promise((resolve) => {
- setTimeout(() => {
- resolve(undefined);
- }, delay);
- });
- return pollTaskResult(apiUrl, apiKey, taskId, delay, timeout - delay);
- }
-}
-
-function isEnhanceTaskStillProcessing(error: unknown): boolean {
- if (!(error instanceof Error)) return false;
- return /轮询超时/i.test(error.message || "");
-}
-
-function buildEnhanceTaskPendingResponse(taskId: string) {
- return NextResponse.json(
- {
- success: true,
- processing: true,
- status: "running",
- taskId,
- pollAfterMs: ENHANCE_TASK_POLL_INTERVAL_MS,
- },
- { status: 202 }
- );
-}
-
-async function finalizeEnhanceTaskResult(params: {
- userId: string;
- taskResult: any;
- prompt?: string;
- chatId?: string | null;
- messageId?: string | null;
- startedAt?: number;
- sourceImageUrls?: string[];
-}) {
- if (params.taskResult?.status !== 200 || !params.taskResult?.data) {
- throw new Error(params.taskResult?.message || "未知错误");
- }
-
- let enhancedImageBase64: string | null = null;
- if (typeof params.taskResult.data.image_base64 === "string" && params.taskResult.data.image_base64) {
- enhancedImageBase64 = params.taskResult.data.image_base64;
- } else if (typeof params.taskResult.data.image === "string" && params.taskResult.data.image) {
- const imageData = params.taskResult.data.image;
- if (/^https?:\/\//i.test(imageData)) {
- const imageResponse = await fetch(imageData);
- if (!imageResponse.ok) {
- throw new Error(`无法下载处理后的图片: ${imageResponse.status}`);
- }
- const downloadedBuffer = await imageResponse.arrayBuffer();
- enhancedImageBase64 = Buffer.from(downloadedBuffer).toString("base64");
- } else {
- enhancedImageBase64 = imageData;
- }
- }
-
- if (!enhancedImageBase64) {
- throw new Error("轮询结果未包含图片数据");
- }
-
- const uploadResult = await uploadBase64ToS3(enhancedImageBase64, "image/jpeg");
- const finalPrompt = params.prompt?.trim() || "图片变清晰处理";
-
- let saved = false;
- let saveError: unknown = null;
- let savedImage:
- | {
- id: string;
- }
- | null = null;
- for (let attempt = 1; attempt <= 3; attempt += 1) {
- try {
- ([savedImage] = await db.insert(generatedImage).values({
- userId: params.userId,
- chatId: params.chatId || null,
- messageId: params.messageId || null,
- thumbnailUrl: uploadResult.thumbnail,
- originalUrl: uploadResult.original,
- imageSize: "2K",
- prompt: finalPrompt,
- modelId: "enhance-1.0",
- }).returning());
- saved = true;
- break;
- } catch (dbError) {
- saveError = dbError;
- if (attempt < 3) {
- await new Promise((resolve) => setTimeout(resolve, 120 * attempt));
- }
- }
- }
- if (!saved) {
- console.error("[Enhance] 保存到相册失败:", saveError);
- throw new Error("图片变清晰成功但保存到相册失败,请稍后重试");
- }
-
- if (savedImage) {
- try {
- const sourceAssets =
- Array.isArray(params.sourceImageUrls) && params.sourceImageUrls.length > 0
- ? await getMediaAssetsByUserAndUrls({
- userId: params.userId,
- urls: params.sourceImageUrls,
- })
- : [];
- const media = await upsertMediaAsset({
- userId: params.userId,
- chatId: params.chatId || null,
- mediaType: "image",
- sourceType: "edited",
- status: "ready",
- originalUrl: uploadResult.original,
- previewUrl: uploadResult.original,
- thumbnailUrl: uploadResult.thumbnail,
- displayName: finalPrompt.slice(0, 80),
- prompt: finalPrompt,
- modelId: "enhance-1.0",
- sourceMessageId: params.messageId || null,
- sourceTable: "GeneratedImage",
- sourceRowId: savedImage.id,
- primaryParentAssetId: sourceAssets[0]?.id ?? null,
- metadata: {
- imageSize: "2K",
- },
- });
- if (media) {
- for (const sourceAsset of sourceAssets) {
- await createMediaAssetRelation({
- userId: params.userId,
- chatId: params.chatId || null,
- fromAssetId: sourceAsset.id,
- toAssetId: media.id,
- relationType: "edited_from",
- sourceMessageId: params.messageId || null,
- metadata: {
- prompt: finalPrompt,
- },
- });
- }
- }
- } catch (assetError) {
- console.error("[Enhance] ⚠️ MediaAsset 注册失败:", assetError);
- }
- }
-
- const processingTime =
- typeof params.startedAt === "number" ? (Date.now() - params.startedAt) / 1000 : undefined;
-
- return {
- success: true,
- image_base64: enhancedImageBase64,
- thumbnail_url: uploadResult.thumbnail,
- original_url: uploadResult.original,
- processing_time: processingTime,
- };
-}
-
-async function executeFalEsrganFallback(params: {
- userId: string;
- imageBase64: string;
- prompt: string;
- chatId: string | null;
- messageId: string | null;
- sourceImageUrl: string | null;
- requestedScaleFactor?: number;
- reason: string;
-}) {
- const scaleFactor = params.requestedScaleFactor || 4;
- console.warn("[Enhance-FAL-ESRGAN] 启用专用高清备用链路", {
- reason: params.reason.slice(0, 500),
- scaleFactor,
- });
- const result = await processWithFalEsrgan(params.imageBase64, scaleFactor);
- const payload = await finalizeEnhanceTaskResult({
- userId: params.userId,
- taskResult: {
- status: 200,
- data: { image_base64: result.imageBase64 },
- },
- prompt: params.prompt || "图片变清晰处理",
- chatId: params.chatId,
- messageId: params.messageId,
- sourceImageUrls: params.sourceImageUrl
- ? [params.sourceImageUrl]
- : undefined,
- });
- return {
- ...payload,
- processing_time: result.processingTime,
- api_type: "fal-esrgan",
- };
-}
-
-export async function GET(request: NextRequest) {
- try {
- const session = await auth();
- if (!session?.user?.id) {
- return NextResponse.json({ error: "未授权,请先登录" }, { status: 401 });
- }
-
- const { searchParams } = new URL(request.url);
- const taskId = String(searchParams.get("taskId") || "").trim();
- if (!taskId) {
- return NextResponse.json({ error: "缺少 taskId 参数" }, { status: 400 });
- }
-
- const prompt = searchParams.get("prompt") || "图片变清晰处理";
- const chatId = searchParams.get("chatId");
- const messageId = searchParams.get("messageId");
- const sourceImageUrl = searchParams.get("sourceImageUrl");
-
- const enhanceConfig = await getSystemSetting({ key: "enhance_api_config" });
- if (!enhanceConfig?.value) {
- return NextResponse.json(
- { error: "变清晰API配置未设置,请在系统设置中配置" },
- { status: 400 }
- );
- }
-
- const config = enhanceConfig.value as EnhanceApiConfig;
- if (config.provider === "vip98k") {
- return NextResponse.json(
- { error: "VIP98K 变清晰任务在 POST 请求内完成,无需 taskId 轮询" },
- { status: 400 }
- );
- }
- if (config.useBackupApi) {
- return NextResponse.json(
- { error: "当前启用了Real-ESRGAN备用模式,不支持 taskId 轮询" },
- { status: 400 }
- );
- }
- if (!config.apiUrl || !config.apiKey) {
- return NextResponse.json(
- { error: "变清晰API地址或密钥未配置" },
- { status: 400 }
- );
- }
-
- const apiUrl = config.apiUrl.endsWith("/") ? config.apiUrl.slice(0, -1) : config.apiUrl;
-
- let taskResult: any;
- try {
- taskResult = await pollTaskResult(
- apiUrl,
- config.apiKey,
- taskId,
- ENHANCE_TASK_POLL_INTERVAL_MS,
- ENHANCE_SYNC_WAIT_TIMEOUT_MS
- );
- } catch (pollError) {
- if (isEnhanceTaskStillProcessing(pollError)) {
- return buildEnhanceTaskPendingResponse(taskId);
- }
- const detail = pollError instanceof Error ? pollError.message : "轮询失败";
- return NextResponse.json(
- { error: "变清晰处理失败", detail },
- { status: 500 }
- );
- }
-
- const responsePayload = await finalizeEnhanceTaskResult({
- userId: session.user.id,
- taskResult,
- prompt,
- chatId,
- messageId,
- sourceImageUrls: sourceImageUrl ? [sourceImageUrl] : undefined,
- });
-
- return NextResponse.json(responsePayload);
- } catch (error) {
- console.error("[Enhance API][TaskStatus] 处理错误:", error);
- return NextResponse.json(
- { error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" },
- { status: 500 }
- );
- }
-}
-
-/**
- * 图片变清晰API路由
- * 从数据库读取变清晰API配置,调用外部API进行图片增强处理
- * 将处理后的图片上传到S3并保存到数据库
- */
-export async function POST(request: NextRequest) {
- console.log("[Enhance API] ===== 收到请求 =====");
- try {
- // 0. 获取用户会话
- console.log("[Enhance API] 开始获取用户会话...");
- const session = await auth();
- console.log("[Enhance API] 用户会话获取完成:", session?.user?.id ? "已登录" : "未登录");
- if (!session?.user?.id) {
- return NextResponse.json(
- { error: "未授权,请先登录" },
- { status: 401 }
- );
- }
-
- const userId = session.user.id;
- console.log("[Enhance API] 用户ID:", userId);
- // 1. 从数据库获取变清晰API配置
- console.log("[Enhance API] 开始获取配置...");
- const enhanceConfig = await getSystemSetting({ key: "enhance_api_config" });
- console.log("[Enhance API] 配置获取完成:", enhanceConfig ? "已找到" : "未找到");
-
- if (!enhanceConfig || !enhanceConfig.value) {
- return NextResponse.json(
- { error: "变清晰API配置未设置,请在系统设置中配置" },
- { status: 400 }
- );
- }
-
- const config = enhanceConfig.value as EnhanceApiConfig;
- const useVip98k = config.provider === "vip98k";
-
- // 检查是否使用备用API
- const useBackupApi = !useVip98k && config.useBackupApi === true;
- console.log(`[Enhance API] 使用${useBackupApi ? "备用API (Real-ESRGAN)" : "主API"}`);
-
- if (useVip98k) {
- // VIP98K 密钥只从 AiProvider 读取。
- } else if (useBackupApi) {
- // 验证备用API配置
- if (!config.backupApiUrl || !config.backupApiKey) {
- return NextResponse.json(
- { error: "备用变清晰API地址或密钥未配置" },
- { status: 400 }
- );
- }
- } else {
- // 验证主API配置
- if (!config.apiUrl || !config.apiKey) {
- return NextResponse.json(
- { error: "变清晰API地址或密钥未配置" },
- { status: 400 }
- );
- }
- }
-
- // 2. 获取请求体(支持 imageUrl、image_base64 或 FormData)
- console.log("[Enhance API] 开始获取请求体...");
- const contentType = request.headers.get("content-type") || "";
- console.log("[Enhance API] Content-Type:", contentType);
-
- let imageBase64: string;
- let prompt = ""; // 用户提示词(可选)
- let chatId: string | null = null; // 聊天ID(可选)
- let messageId: string | null = null; // 消息ID(可选)
- let sourceImageUrl: string | null = null;
- let requestedScaleFactor: number | undefined;
-
- if (contentType.includes("application/json")) {
- console.log("[Enhance API] 解析JSON请求体...");
- const body = await request.json();
- prompt = body.prompt || "图片变清晰处理";
- chatId = body.chatId || null;
- messageId = body.messageId || null;
- const parsedScaleFactor = Number.parseInt(String(body.scaleFactor || ""), 10);
- requestedScaleFactor =
- Number.isFinite(parsedScaleFactor) && [1, 2, 4, 8].includes(parsedScaleFactor)
- ? parsedScaleFactor
- : undefined;
-
- // 优先使用 imageUrl,其次使用 image_base64
- if (body.imageUrl) {
- sourceImageUrl = String(body.imageUrl);
- // 从 URL 获取图片并转换为 Base64
- console.log(`[Enhance API] 从 URL 获取图片: ${body.imageUrl}`);
- try {
- const imageResponse = await fetch(body.imageUrl);
- if (!imageResponse.ok) {
- return NextResponse.json(
- { error: `无法获取图片: ${imageResponse.status} ${imageResponse.statusText}` },
- { status: 400 }
- );
- }
- const arrayBuffer = await imageResponse.arrayBuffer();
- const buffer = Buffer.from(arrayBuffer);
- imageBase64 = buffer.toString("base64");
- console.log(`[Enhance API] 图片已转换为 Base64,大小: ${buffer.length} bytes`);
- } catch (fetchError) {
- console.error("[Enhance API] 获取图片失败:", fetchError);
- return NextResponse.json(
- { error: "无法获取图片,请检查图片链接是否有效" },
- { status: 400 }
- );
- }
- } else if (body.image_base64) {
- console.log("[Enhance API] JSON请求体解析完成,image_base64长度:", body.image_base64?.length || 0);
- imageBase64 = body.image_base64;
- } else {
- return NextResponse.json(
- { error: "缺少 imageUrl 或 image_base64 参数" },
- { status: 400 }
- );
- }
- } else if (contentType.includes("multipart/form-data")) {
- // FormData格式:文件上传
- const formData = await request.formData();
- const file = formData.get("file") as File;
-
- if (!file) {
- return NextResponse.json(
- { error: "缺少file参数" },
- { status: 400 }
- );
- }
-
- // 将文件转换为Base64
- const arrayBuffer = await file.arrayBuffer();
- const buffer = Buffer.from(arrayBuffer);
- imageBase64 = buffer.toString("base64");
- prompt = (formData.get("prompt") as string) || "图片变清晰处理";
- chatId = (formData.get("chatId") as string) || null;
- messageId = (formData.get("messageId") as string) || null;
- } else {
- return NextResponse.json(
- { error: "不支持的Content-Type,请使用application/json或multipart/form-data" },
- { status: 400 }
- );
- }
-
- if (useVip98k) {
- try {
- const scaleFactor = requestedScaleFactor || 4;
- const payload = await processWithVip98kEnhance({
- userId,
- imageBase64,
- prompt: prompt || "图片变清晰处理",
- chatId,
- messageId,
- sourceImageUrl,
- scaleFactor,
- });
- return NextResponse.json(payload);
- } catch (error) {
- console.error("[Enhance-VIP98K] Processing failed", error);
- return NextResponse.json(
- {
- error: "VIP98K 变清晰处理失败",
- detail: error instanceof Error ? error.message : "未知错误",
- },
- { status: 500 }
- );
- }
- }
-
- // ========== 备用API处理路径 (Real-ESRGAN) ==========
- if (useBackupApi) {
- console.log("[Enhance API] 使用Real-ESRGAN备用API处理(URL方式)...");
-
- try {
- // 步骤1: 先将Base64图片上传到S3获取公开可访问的URL
- console.log(`[Enhance-RealESRGAN] 📤 步骤1: 上传原图到S3获取URL...`);
- const tempUploadResult = await uploadBase64ToS3(imageBase64, "image/jpeg");
- const sourceImageUrl = tempUploadResult.original;
- console.log(`[Enhance-RealESRGAN] ✅ 原图已上传,URL: ${sourceImageUrl}`);
-
- // 步骤2: 使用URL方式调用Real-ESRGAN API
- console.log(`[Enhance-RealESRGAN] 🔄 步骤2: 调用Real-ESRGAN API处理...`);
- const result = await processWithRealESRGAN(sourceImageUrl, {
- backupApiUrl: config.backupApiUrl!,
- backupApiKey: config.backupApiKey!,
- backupScale: String(requestedScaleFactor || Number.parseInt(config.backupScale || "4", 10) || 4),
- backupModel: config.backupModel || "RealESRGAN_x4plus",
- });
-
- // 步骤3: Real-ESRGAN API返回的URL就是处理后的图片
- // 需要重新上传到我们的S3(因为Real-ESRGAN返回的URL可能在另一个S3桶)
- console.log(`[Enhance-RealESRGAN] 📤 步骤3: 上传处理后的图片到S3...`);
- let thumbnailUrl: string;
- let originalUrl: string;
-
- try {
- const uploadResult = await uploadBase64ToS3(result.imageBase64, "image/png");
- thumbnailUrl = uploadResult.thumbnail;
- originalUrl = uploadResult.original;
- console.log(`[Enhance-RealESRGAN] ✅ 处理后图片已上传: ${originalUrl}`);
- } catch (uploadError) {
- console.error("[Enhance-RealESRGAN] ❌ 上传到S3失败:", uploadError);
- // 如果上传失败,直接使用Real-ESRGAN返回的URL
- console.log(`[Enhance-RealESRGAN] ⚠️ 使用Real-ESRGAN返回的URL作为备选: ${result.imageUrl}`);
- thumbnailUrl = result.imageUrl;
- originalUrl = result.imageUrl;
- }
-
- // 步骤4: 保存到数据库
- let saved = false;
- let saveError: unknown = null;
- let savedImage:
- | {
- id: string;
- }
- | null = null;
- for (let attempt = 1; attempt <= 3; attempt += 1) {
- try {
- ([savedImage] = await db.insert(generatedImage).values({
- userId,
- chatId: chatId || null,
- messageId: messageId || null,
- thumbnailUrl,
- originalUrl,
- imageSize: `${requestedScaleFactor || Number.parseInt(config.backupScale || "4", 10) || 4}x放大`,
- prompt: prompt || "Real-ESRGAN图片变清晰处理",
- modelId: `realesrgan-${config.backupModel}`,
- }).returning());
- console.log(`[Enhance-RealESRGAN] ✅ 保存到数据库成功,ID: ${savedImage.id}`);
- saved = true;
- break;
- } catch (dbError) {
- saveError = dbError;
- if (attempt < 3) {
- await new Promise((resolve) => setTimeout(resolve, 120 * attempt));
- }
- }
- }
- if (!saved) {
- console.error("[Enhance-RealESRGAN] ❌ 保存到数据库失败:", saveError);
- throw new Error("图片变清晰成功但保存到相册失败,请稍后重试");
- }
-
- if (savedImage) {
- try {
- const sourceAssets =
- sourceImageUrl
- ? await getMediaAssetsByUserAndUrls({
- userId,
- urls: [sourceImageUrl],
- })
- : [];
- const media = await upsertMediaAsset({
- userId,
- chatId,
- mediaType: "image",
- sourceType: "edited",
- status: "ready",
- originalUrl,
- previewUrl: originalUrl,
- thumbnailUrl,
- displayName: (prompt || "Real-ESRGAN图片变清晰处理").slice(0, 80),
- prompt: prompt || "Real-ESRGAN图片变清晰处理",
- modelId: `realesrgan-${config.backupModel}`,
- sourceMessageId: messageId,
- sourceTable: "GeneratedImage",
- sourceRowId: savedImage.id,
- primaryParentAssetId: sourceAssets[0]?.id ?? null,
- metadata: {
- imageSize: `${requestedScaleFactor || Number.parseInt(config.backupScale || "4", 10) || 4}x放大`,
- apiType: "realesrgan-url",
- },
- });
- if (media) {
- for (const sourceAsset of sourceAssets) {
- await createMediaAssetRelation({
- userId,
- chatId,
- fromAssetId: sourceAsset.id,
- toAssetId: media.id,
- relationType: "edited_from",
- sourceMessageId: messageId,
- metadata: {
- prompt: prompt || "Real-ESRGAN图片变清晰处理",
- },
- });
- }
- }
- } catch (assetError) {
- console.error("[Enhance-RealESRGAN] ⚠️ MediaAsset 注册失败:", assetError);
- }
- }
-
- // 返回结果
- console.log(`[Enhance-RealESRGAN] ✅ 全部完成,总耗时: ${result.processingTime.toFixed(2)}秒`);
- return NextResponse.json({
- success: true,
- image_base64: result.imageBase64,
- thumbnail_url: thumbnailUrl,
- original_url: originalUrl,
- processing_time: result.processingTime,
- api_type: "realesrgan-url",
- });
-
- } catch (error) {
- console.error("[Enhance-RealESRGAN] ❌ 处理失败:", error);
- try {
- const fallbackPayload = await executeFalEsrganFallback({
- userId,
- imageBase64,
- prompt,
- chatId,
- messageId,
- sourceImageUrl,
- requestedScaleFactor,
- reason: error instanceof Error ? error.message : "RealESRGAN 备用服务失败",
- });
- return NextResponse.json(fallbackPayload);
- } catch (falError) {
- console.error("[Enhance-FAL-ESRGAN] 专用高清备用链路失败:", falError);
- }
- return NextResponse.json(
- {
- error: "Real-ESRGAN变清晰处理失败",
- detail: error instanceof Error ? error.message : "未知错误",
- },
- { status: 500 }
- );
- }
- }
-
- // ========== 主API处理路径 ==========
- // 3. 调用变清晰API(使用异步模式,根据官方示例)
- console.log("[Enhance API] 开始准备主API调用...");
- const apiUrl = config.apiUrl!.endsWith("/")
- ? config.apiUrl!.slice(0, -1)
- : config.apiUrl!;
-
- const enhanceUrl = `${apiUrl}/api/tasks/visual/scale`;
- console.log("[Enhance API] API URL:", enhanceUrl);
-
- // 将Base64转换为Buffer
- console.log("[Enhance API] 转换Base64为Buffer,长度:", imageBase64.length);
- const rawImageBuffer = Buffer.from(imageBase64, "base64");
- const imageBuffer = await ensureAosResolutionLimit(rawImageBuffer, "Enhance API");
- console.log("[Enhance API] Buffer创建完成,大小:", imageBuffer.length, "bytes");
-
- // 创建FormData(使用form-data包,Node.js环境)
- console.log("[Enhance API] 创建FormData...");
- const FormDataLib = await import("form-data");
- const formData = new FormDataLib.default();
- console.log("[Enhance API] FormData创建完成");
- formData.append("image_file", imageBuffer, {
- filename: "image.jpg",
- contentType: "image/jpeg",
- });
- // 使用异步模式(sync=0),根据官方示例
- formData.append("sync", "0");
- formData.append("type", config.type || "clean"); // 默认通用清晰化
- const scaleFactorValue =
- requestedScaleFactor ||
- (typeof config.scaleFactor === "number"
- ? config.scaleFactor
- : config.scaleFactor === "auto"
- ? undefined
- : Number.parseInt(String(config.scaleFactor)));
- if (scaleFactorValue) {
- formData.append("scale_factor", String(scaleFactorValue));
- }
- formData.append("return_type", "2"); // 返回base64
-
- const headers: Record = {
- "X-API-KEY": config.apiKey!,
- ...formData.getHeaders(), // 包含Content-Type和boundary
- };
-
- const startTime = Date.now();
- let taskId: string;
-
- try {
- console.log("[Enhance API] 开始创建任务...");
- // 将FormData转换为Buffer(form-data包是stream)
- console.log("[Enhance API] 转换FormData为Buffer...");
- const formDataBuffer = await new Promise((resolve, reject) => {
- const chunks: Buffer[] = [];
- formData.on("data", (chunk: any) => {
- // 确保chunk是Buffer类型
- if (Buffer.isBuffer(chunk)) {
- chunks.push(chunk);
- } else if (typeof chunk === "string") {
- chunks.push(Buffer.from(chunk, "utf-8"));
- } else if (chunk instanceof Uint8Array) {
- chunks.push(Buffer.from(chunk));
- } else {
- console.warn("[Enhance API] 未知的chunk类型:", typeof chunk, chunk);
- chunks.push(Buffer.from(String(chunk), "utf-8"));
- }
- });
- formData.on("end", () => {
- console.log("[Enhance API] FormData stream结束,chunks数量:", chunks.length);
- resolve(Buffer.concat(chunks));
- });
- formData.on("error", (error) => {
- console.error("[Enhance API] FormData stream错误:", error);
- reject(error);
- });
-
- // 重要:form-data包需要被pipe或手动触发才能开始流式传输
- // 使用PassThrough来触发stream
- const { PassThrough } = require("stream");
- const passThrough = new PassThrough();
- passThrough.on("data", () => {}); // 忽略数据,我们已经在formData上监听
- formData.pipe(passThrough);
- });
-
- console.log("[Enhance API] FormData Buffer转换完成,大小:", formDataBuffer.length, "bytes");
- const uint8Array = new Uint8Array(formDataBuffer);
-
- let createResponse: Response | null = null;
- let lastFetchError: unknown;
-
- for (let attempt = 1; attempt <= ENHANCE_CREATE_RETRY_COUNT + 1; attempt++) {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), ENHANCE_CREATE_TIMEOUT_MS);
-
- try {
- console.log(`[Enhance API] 发送创建任务请求... attempt=${attempt}`);
- createResponse = await fetch(enhanceUrl, {
- method: "POST",
- headers,
- body: uint8Array,
- signal: controller.signal,
- });
- console.log("[Enhance API] 创建任务响应状态:", createResponse.status, createResponse.statusText);
- clearTimeout(timeoutId);
- break;
- } catch (fetchError) {
- clearTimeout(timeoutId);
- lastFetchError = fetchError;
- const canRetry = attempt <= ENHANCE_CREATE_RETRY_COUNT && isRetryableNetworkError(fetchError);
- if (canRetry) {
- console.warn(
- `[Enhance API] 创建任务失败,准备重试: attempt=${attempt}, error=${fetchError instanceof Error ? fetchError.message : String(fetchError)}`
- );
- await new Promise((resolve) => setTimeout(resolve, 1500));
- continue;
- }
- throw fetchError;
- }
- }
-
- if (!createResponse) {
- throw lastFetchError instanceof Error ? lastFetchError : new Error("创建任务请求失败");
- }
-
- if (!createResponse.ok) {
- const errorText = await createResponse.text();
- throw new Error(`创建任务失败: ${errorText || createResponse.statusText}`);
- }
-
- const createResult = await createResponse.json();
-
- if (!createResult.data || !createResult.data.task_id) {
- throw new Error(`API未返回task_id: ${JSON.stringify(createResult)}`);
- }
-
- taskId = createResult.data.task_id;
- console.log(`[Enhance] 任务已创建,task_id: ${taskId}`);
- } catch (fetchError) {
- const processingTime = (Date.now() - startTime) / 1000;
- console.error("[Enhance API] 网络请求失败:", {
- error: fetchError,
- enhanceUrl,
- processingTime,
- });
-
- try {
- const fallbackPayload = await executeFalEsrganFallback({
- userId,
- imageBase64,
- prompt,
- chatId,
- messageId,
- sourceImageUrl,
- requestedScaleFactor,
- reason: fetchError instanceof Error ? fetchError.message : "AOS 高清任务创建失败",
- });
- return NextResponse.json(fallbackPayload);
- } catch (falError) {
- console.error("[Enhance-FAL-ESRGAN] 专用高清备用链路失败:", falError);
- }
-
- // 检查是否是网络连接错误
- if (fetchError instanceof Error) {
- if (fetchError.name === "AbortError") {
- return NextResponse.json(
- {
- error: "变清晰API请求超时",
- detail: "创建任务请求超过120秒未响应,请检查API服务器状态或网络连接",
- },
- { status: 504 }
- );
- }
-
- // 检查错误消息和cause中的网络错误
- const errorMessage = fetchError.message || "";
- const errorCause = (fetchError as any).cause;
- const causeMessage = errorCause?.message || "";
- const causeCode = errorCause?.code || "";
-
- // 检查各种网络连接错误
- if (errorMessage.includes("EHOSTUNREACH") ||
- errorMessage.includes("ENOTFOUND") ||
- errorMessage.includes("ECONNREFUSED") ||
- errorMessage.includes("No route to host") ||
- causeMessage.includes("EHOSTUNREACH") ||
- causeMessage.includes("ENOTFOUND") ||
- causeMessage.includes("ECONNREFUSED") ||
- causeMessage.includes("No route to host") ||
- causeCode === "EHOSTUNREACH" ||
- causeCode === "ENOTFOUND" ||
- causeCode === "ECONNREFUSED") {
- return NextResponse.json(
- {
- error: "无法连接到变清晰API服务器",
- detail: `服务器无法访问 ${enhanceUrl},请检查:\n1. API服务器是否在线\n2. 网络连接是否正常\n3. 防火墙设置是否正确\n4. API地址和密钥是否正确`,
- },
- { status: 503 }
- );
- }
- }
-
- return NextResponse.json(
- {
- error: "变清晰API请求失败",
- detail: fetchError instanceof Error ? fetchError.message : "未知网络错误",
- },
- { status: 500 }
- );
- }
-
- // 4. 轮询获取任务结果(短等待,超时后让前端继续轮询)
- console.log(`[Enhance] 开始短轮询任务结果,task_id: ${taskId}`);
- let taskResult: any;
- try {
- taskResult = await pollTaskResult(
- apiUrl,
- config.apiKey!,
- taskId,
- ENHANCE_TASK_POLL_INTERVAL_MS,
- ENHANCE_SYNC_WAIT_TIMEOUT_MS
- );
- } catch (pollError) {
- if (isEnhanceTaskStillProcessing(pollError)) {
- return buildEnhanceTaskPendingResponse(taskId);
- }
- const detail = pollError instanceof Error ? pollError.message : "轮询失败";
- try {
- const fallbackPayload = await executeFalEsrganFallback({
- userId,
- imageBase64,
- prompt,
- chatId,
- messageId,
- sourceImageUrl,
- requestedScaleFactor,
- reason: detail,
- });
- return NextResponse.json(fallbackPayload);
- } catch (falError) {
- console.error("[Enhance-FAL-ESRGAN] 专用高清备用链路失败:", falError);
- }
- const friendlyMessage = getAosFriendlyErrorMessage(detail);
- return NextResponse.json(
- { error: "变清晰处理失败", detail: friendlyMessage || detail },
- { status: friendlyMessage ? 400 : 500 }
- );
- }
-
- const responsePayload = await finalizeEnhanceTaskResult({
- userId,
- taskResult,
- prompt,
- chatId,
- messageId,
- startedAt: startTime,
- sourceImageUrls: sourceImageUrl ? [sourceImageUrl] : undefined,
- });
-
- return NextResponse.json(responsePayload);
- } catch (error) {
- console.error("[Enhance API] 处理错误:", error);
- return NextResponse.json(
- { error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" },
- { status: 500 }
- );
- }
+export async function POST(request: Request) {
+ const session = await auth();
+ if (!session?.user?.id) return NextResponse.json({ error: "未登录" }, { status: 401 });
+ return executeEnhancePOST(request, session.user.id);
}
diff --git a/app/api/extend/route.ts b/app/api/extend/route.ts
index 196d74172..fb215ae44 100644
--- a/app/api/extend/route.ts
+++ b/app/api/extend/route.ts
@@ -1,239 +1,10 @@
-import { type NextRequest, NextResponse } from "next/server";
+import { NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
-import { ExtendServiceError, executeExtendJob } from "@/lib/ai/extend/service";
-import { getPointsSystemConfig } from "@/lib/db/queries";
-import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
-import { getImageToolCreditCost } from "@/lib/points/config";
-import {
- getFriendlyRateLimitMessage,
- getUserFacingRequestErrorMessage,
- isRateLimitError,
-} from "@/lib/request-error";
-
+import { executeExtendPOST } from "@/lib/canvas/operations/extend";
+export const runtime = "nodejs";
export const maxDuration = 180;
-
-function parseIncludeBase64(value: unknown): boolean {
- if (typeof value === "boolean") return value;
- const normalized = String(value ?? "").trim().toLowerCase();
- return normalized === "true" || normalized === "1" || normalized === "yes";
-}
-
-function buildExtendSuccessPayload(result: Awaited>) {
- return {
- success: true,
- original_url: result.original_url,
- thumbnail_url: result.thumbnail_url,
- ...(result.image_base64 ? { base64: result.image_base64 } : {}),
- processing_time: result.processing_time,
- };
-}
-
-export async function POST(request: NextRequest) {
- try {
- const session = await auth();
- if (!session?.user?.id) {
- return NextResponse.json({ error: "未授权,请先登录" }, { status: 401 });
- }
-
- const contentType = request.headers.get("content-type") || "";
-
- let imageUrl: string | null = null;
- let imageBase64: string | null = null;
- let prompt = "图像拓展处理";
- let chatId: string | null = null;
- let messageId: string | null = null;
- let includeBase64 = false;
- const requestId = `extend-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
-
- if (contentType.includes("application/json")) {
- const body = await request.json();
- imageUrl = body.image_url ? String(body.image_url).trim() : null;
- imageBase64 = body.image_base64 ? String(body.image_base64).trim() : null;
- prompt = body.prompt || prompt;
- chatId = body.chatId || null;
- messageId = body.messageId || null;
- includeBase64 = parseIncludeBase64(body.includeBase64);
-
- if (!imageUrl && !imageBase64) {
- return NextResponse.json(
- { error: "缺少image_base64或image_url参数" },
- { status: 400 },
- );
- }
-
- console.log("[Extend API] Request received:", {
- requestId,
- source: imageUrl ? "url" : "base64",
- includeBase64,
- chatId,
- messageId,
- xScale: body.xScale,
- yScale: body.yScale,
- });
-
- const pointsConfig = await getPointsSystemConfig();
- const pointsCharge = await prepareFeaturePointsCharge({
- userId: session.user.id,
- unitCost: getImageToolCreditCost(pointsConfig, "extend"),
- reason: "image_extend",
- modelId: "image-extend",
- note: "图片扩图消耗积分",
- pointsConfig,
- });
- if (!pointsCharge.ok) {
- return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status });
- }
-
- const result = await executeExtendJob({
- userId: session.user.id,
- imageUrl,
- imageBase64,
- prompt,
- chatId,
- messageId,
- xScale: body.xScale ? Number(body.xScale) : undefined,
- yScale: body.yScale ? Number(body.yScale) : undefined,
- includeBase64,
- requestId,
- });
-
- if (pointsCharge.chargeable) {
- try {
- await pointsCharge.consume();
- } catch (pointsError) {
- console.error("[Points] Failed to consume extend points:", pointsError);
- }
- }
-
- console.log("[Extend API] Request completed:", {
- requestId,
- includeBase64,
- processingTime: result.processing_time,
- originalUrl: result.original_url.slice(0, 120),
- });
-
- return NextResponse.json(buildExtendSuccessPayload(result));
- }
-
- if (contentType.includes("multipart/form-data")) {
- const formData = await request.formData();
- const file = formData.get("file") as File | null;
- if (!file) {
- return NextResponse.json({ error: "缺少file参数" }, { status: 400 });
- }
-
- const arrayBuffer = await file.arrayBuffer();
- const buffer = Buffer.from(arrayBuffer);
- const multipartChatId = String(formData.get("chatId") || "") || null;
- const multipartMessageId = String(formData.get("messageId") || "") || null;
- includeBase64 = parseIncludeBase64(formData.get("includeBase64"));
- console.log("[Extend API] Multipart request received:", {
- requestId,
- includeBase64,
- chatId: multipartChatId,
- messageId: multipartMessageId,
- bytes: buffer.length,
- });
-
- const pointsConfig = await getPointsSystemConfig();
- const pointsCharge = await prepareFeaturePointsCharge({
- userId: session.user.id,
- unitCost: getImageToolCreditCost(pointsConfig, "extend"),
- reason: "image_extend",
- modelId: "image-extend",
- note: "图片扩图消耗积分",
- pointsConfig,
- });
- if (!pointsCharge.ok) {
- return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status });
- }
-
- const result = await executeExtendJob({
- userId: session.user.id,
- imageBase64: buffer.toString("base64"),
- prompt: String(formData.get("prompt") || prompt),
- chatId: multipartChatId,
- messageId: multipartMessageId,
- includeBase64,
- requestId,
- });
-
- if (pointsCharge.chargeable) {
- try {
- await pointsCharge.consume();
- } catch (pointsError) {
- console.error("[Points] Failed to consume extend points:", pointsError);
- }
- }
-
- console.log("[Extend API] Multipart request completed:", {
- requestId,
- includeBase64,
- processingTime: result.processing_time,
- originalUrl: result.original_url.slice(0, 120),
- });
-
- return NextResponse.json(buildExtendSuccessPayload(result));
- }
-
- return NextResponse.json(
- { error: "不支持的Content-Type,请使用application/json或multipart/form-data" },
- { status: 400 },
- );
- } catch (error) {
- if (error instanceof ExtendServiceError) {
- console.error("[Extend API] Request failed:", {
- status: error.status,
- message: error.message,
- detail: error.detail,
- });
- if (error.status === 429 || isRateLimitError(error)) {
- return NextResponse.json(
- {
- error: getFriendlyRateLimitMessage("当前操作过于频繁,请稍后再试"),
- detail: error.detail || error.message,
- retryAfter: 10,
- },
- {
- status: 429,
- headers: {
- "Retry-After": "10",
- },
- },
- );
- }
-
- return NextResponse.json(
- {
- error: getUserFacingRequestErrorMessage(error, "图像拓展失败"),
- detail: error.detail || error.message,
- },
- { status: error.status },
- );
- }
-
- console.error("[Extend API] 错误:", error);
- if (isRateLimitError(error)) {
- return NextResponse.json(
- {
- error: getFriendlyRateLimitMessage("当前操作过于频繁,请稍后再试"),
- detail: error instanceof Error ? error.message : String(error),
- retryAfter: 10,
- },
- {
- status: 429,
- headers: {
- "Retry-After": "10",
- },
- },
- );
- }
- return NextResponse.json(
- {
- error: getUserFacingRequestErrorMessage(error, "图像拓展失败"),
- detail: error instanceof Error ? error.message : String(error),
- },
- { status: 500 },
- );
- }
+export async function POST(request: Request) {
+ const session = await auth();
+ if (!session?.user?.id) return NextResponse.json({ error: "未登录" }, { status: 401 });
+ return executeExtendPOST(request, session.user.id);
}
diff --git a/app/api/rmbg/route.ts b/app/api/rmbg/route.ts
index 889f44c42..2b2e3caeb 100644
--- a/app/api/rmbg/route.ts
+++ b/app/api/rmbg/route.ts
@@ -1,138 +1,10 @@
-import { NextRequest, NextResponse } from "next/server";
-
+import { NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
-import {
- executeRmbgJob,
- fetchImageBufferFromUrl,
- RmbgServiceError,
-} from "@/lib/ai/rmbg/service";
-import { getPointsSystemConfig } from "@/lib/db/queries";
-import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
-import { getImageToolCreditCost } from "@/lib/points/config";
-
+import { executeRemoveBackgroundPOST } from "@/lib/canvas/operations/rmbg";
export const runtime = "nodejs";
export const maxDuration = 60;
-
-export async function POST(request: NextRequest) {
- try {
- const session = await auth();
- if (!session?.user?.id) {
- return NextResponse.json({ error: "未授权,请先登录" }, { status: 401 });
- }
-
- const contentType = request.headers.get("content-type") || "";
-
- let imageBuffer: Buffer;
- let imageBase64: string | undefined;
- let prompt = "抠图处理";
- let chatId: string | null = null;
- let messageId: string | null = null;
- let skipGallerySave = false;
- let sourceImageUrl: string | null = null;
-
- if (contentType.includes("application/json")) {
- const body = await request.json();
- prompt = body.prompt || "抠图处理";
- chatId = body.chatId || null;
- messageId = body.messageId || null;
- skipGallerySave = body.skipGallerySave === true || body.skip_gallery_save === 1;
-
- const imageUrl = body.imageUrl || body.image_url;
- const explicitSourceImageUrl = body.sourceImageUrl || body.source_image_url;
-
- if (imageUrl) {
- sourceImageUrl = String(imageUrl);
- imageBuffer = await fetchImageBufferFromUrl(sourceImageUrl);
- } else if (body.image_base64) {
- imageBase64 = String(body.image_base64);
- imageBuffer = Buffer.from(imageBase64, "base64");
- if (explicitSourceImageUrl) {
- sourceImageUrl = String(explicitSourceImageUrl);
- }
- } else {
- return NextResponse.json(
- { error: "缺少 imageUrl 或 image_base64 参数" },
- { status: 400 }
- );
- }
- } else if (contentType.includes("multipart/form-data")) {
- const formData = await request.formData();
- const file = formData.get("file") as File | null;
-
- if (!file) {
- return NextResponse.json({ error: "缺少file参数" }, { status: 400 });
- }
-
- const arrayBuffer = await file.arrayBuffer();
- imageBuffer = Buffer.from(arrayBuffer);
- imageBase64 = imageBuffer.toString("base64");
- prompt = (formData.get("prompt") as string) || "抠图处理";
- chatId = (formData.get("chatId") as string) || null;
- messageId = (formData.get("messageId") as string) || null;
- skipGallerySave =
- formData.get("skipGallerySave") === "true" || formData.get("skip_gallery_save") === "1";
- sourceImageUrl =
- (formData.get("sourceImageUrl") as string) ||
- (formData.get("source_image_url") as string) ||
- null;
- } else {
- return NextResponse.json(
- { error: "不支持的Content-Type,请使用application/json或multipart/form-data" },
- { status: 400 }
- );
- }
-
- const pointsConfig = await getPointsSystemConfig();
- const pointsCharge = await prepareFeaturePointsCharge({
- userId: session.user.id,
- unitCost: getImageToolCreditCost(pointsConfig, "remove-background"),
- reason: "image_remove_background",
- modelId: "rmbg",
- note: "图片抠图消耗积分",
- pointsConfig,
- });
- if (!pointsCharge.ok) {
- return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status });
- }
-
- const result = await executeRmbgJob({
- userId: session.user.id,
- imageBuffer,
- imageBase64,
- prompt,
- chatId,
- messageId,
- skipGallerySave,
- sourceImageUrl,
- });
-
- if (pointsCharge.chargeable) {
- try {
- await pointsCharge.consume();
- } catch (pointsError) {
- console.error("[Points] Failed to consume RMBG points:", pointsError);
- }
- }
-
- return NextResponse.json({
- success: true,
- ...result,
- });
- } catch (error) {
- if (error instanceof RmbgServiceError) {
- return NextResponse.json(
- {
- error: error.message,
- detail: error.detail || error.message,
- },
- { status: error.status }
- );
- }
-
- console.error("[RMBG API] 处理错误:", error);
- return NextResponse.json(
- { error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" },
- { status: 500 }
- );
- }
+export async function POST(request: Request) {
+ const session = await auth();
+ if (!session?.user?.id) return NextResponse.json({ error: "未登录" }, { status: 401 });
+ return executeRemoveBackgroundPOST(request, session.user.id);
}
diff --git a/components/canvas/LiblibVideoCanvasReplica.tsx b/components/canvas/LiblibVideoCanvasReplica.tsx
index 410d4d6f3..22489777e 100644
--- a/components/canvas/LiblibVideoCanvasReplica.tsx
+++ b/components/canvas/LiblibVideoCanvasReplica.tsx
@@ -2720,6 +2720,7 @@ function getStoryScriptRows(node: CloneNode | null | undefined): StoryScriptRow[
}
type StoryWorkflowAsset = {
+ id?: string;
description: string;
imageUrl: string;
name: string;
@@ -2795,6 +2796,9 @@ function StoryScriptWorkflowDialog({
imageNodes,
imageModel,
analysisModel,
+ taskBusy = false,
+ taskError,
+ onSubmitTask,
}: {
node: CloneNode;
onClose: () => void;
@@ -2805,6 +2809,17 @@ function StoryScriptWorkflowDialog({
imageNodes: CloneNode[];
imageModel?: string;
analysisModel?: string;
+ taskBusy?: boolean;
+ taskError?: string;
+ onSubmitTask?: (
+ kind: "asset" | "prompts",
+ data: {
+ assets: StoryWorkflowAsset[];
+ rows: StoryScriptRow[];
+ assetIndex?: number;
+ style: string;
+ },
+ ) => Promise;
}) {
const rows = getStoryScriptRows(node);
const detected = getStoryWorkflowAssetGroups(rows);
@@ -2831,62 +2846,29 @@ function StoryScriptWorkflowDialog({
rows: latest.current.rows.map((row, i) => (i === index ? { ...row, ...patch } : row)),
});
const compose = async (index?: number) => {
- if (busy) return;
+ if (busy || taskBusy) return;
const source = index === undefined ? rows : [rows[index]];
if (!source.length) return;
setBusy("prompts");
setError("");
try {
- const response = await workflowAnalysisService.analyze({
- mode: "story-script",
- preferredModelRef: analysisModel,
- prompt: `为以下镜头完善 imagePrompt 和 motionPrompt。保持镜头数量、顺序、故事内容和时长不变。返回 analysisRows。风格:${style || "遵循原脚本"}。资产描述:${JSON.stringify(assets.map(({ name, description, type }) => ({ name, description, type })))}。镜头:${JSON.stringify(source)}`,
- imageUrls: assets
- .map((asset) => asset.imageUrl)
- .filter(Boolean)
- .slice(0, 12),
- nodeTitle: node.title,
- });
- const result = response.data?.analysisRows;
- if (
- !response.success ||
- !result ||
- result.length !== source.length ||
- result.some((row) => !row.imagePrompt || !row.motionPrompt)
- )
- throw Error(response.error || "未返回完整镜头提示词,原内容已保留");
- const next = latest.current.rows.map((row, i) => {
- const generated = index === undefined ? result[i] : i === index ? result[0] : null;
- return generated
- ? { ...row, imagePrompt: generated.imagePrompt!, motionPrompt: generated.motionPrompt! }
- : row;
- });
- onPatch({ rows: next });
- toast.success(`已合成 ${result.length} 个镜头提示词`);
- } catch (e) {
- setError(e instanceof Error ? e.message : "提示词合成失败");
+ if (!onSubmitTask) throw Error("任务服务尚未就绪");
+ await onSubmitTask("prompts", { assets, rows: source, style });
+ } catch (error) {
+ setError(error instanceof Error ? error.message : "提示词提交失败");
} finally {
setBusy(null);
}
};
const generateAsset = async (index: number) => {
- if (busy) return;
- setBusy(`asset-${index}`);
+ if (busy || taskBusy) return;
+ setBusy("asset");
setError("");
try {
- const asset = assets[index];
- const response = await imageGenerationService.generateImage({
- allowFailover: false,
- model: imageModel,
- imageCount: 1,
- prompt: [asset.name, asset.description, style].filter(Boolean).join(","),
- aspectRatio: "1:1",
- });
- const url = response.success ? getFirstGeneratedImageUrl(response) : "";
- if (!url) throw Error(response.error || "资产图片生成失败");
- updateAsset(index, { imageUrl: url });
- } catch (e) {
- setError(e instanceof Error ? e.message : "资产生成失败");
+ if (!onSubmitTask) throw Error("任务服务尚未就绪");
+ await onSubmitTask("asset", { assets, rows, style, assetIndex: index });
+ } catch (error) {
+ setError(error instanceof Error ? error.message : "资产提交失败");
} finally {
setBusy(null);
}
@@ -2947,7 +2929,29 @@ function StoryScriptWorkflowDialog({
{error}
)}
-