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}

)} -
+ {taskError ? ( +

+ {taskError} +

+ ) : null} + {taskBusy ? ( +

+ 生成任务处理中,可关闭窗口,结果会自动保存并同步。 +

+ ) : null} + {Object.values( + (node.draftValues?.storyTaskResults || {}) as Record< + string, + { status: string; message: string } + >, + ) + .slice(-1) + .map((result, index) => ( +

+ {result.message} +

+ ))} +
{step === "assets" ? ( <>