import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { db } from "@/lib/db"; import { systemSettings, generatedImage } from "@/lib/db/schema"; import { eq } from "drizzle-orm"; import { uploadFile } from "@/lib/storage/service"; import { createMediaAssetRelation, getPointsSystemConfig, getMediaAssetsByUserAndUrls, upsertMediaAsset, } from "@/lib/db/queries"; import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge"; import { getImageToolCreditCost } from "@/lib/points/config"; import { generateCanvasImage } from "@/lib/canvas/generate-image"; import { FAL_NANO_BANANA_2_EDIT_MODEL_ID } from "@/lib/ai/fal-image-models"; export const maxDuration = 600; // PicWish API 需要公网可访问的 HTTP/HTTPS URL const WATERMARK_SYNC_WAIT_TIMEOUT_MS = 20_000; const WATERMARK_POLL_INTERVAL_MS = 1200; // 获取去水印API配置 async function getWatermarkRemoveConfig() { try { const setting = await db .select() .from(systemSettings) .where(eq(systemSettings.key, "watermark_remove_config")) .limit(1); if (setting.length > 0 && setting[0].value) { return setting[0].value as { apiKey: string }; } return null; } catch (error) { console.error("[WatermarkRemove] 获取配置失败:", error); return null; } } // 轮询获取处理结果 async function pollForResult( taskId: string, apiKey: string, timeoutMs = WATERMARK_SYNC_WAIT_TIMEOUT_MS ): Promise<{ success: boolean; imageUrl?: string; error?: string; pending?: boolean }> { const maxAttempts = Math.max(1, Math.ceil(timeoutMs / WATERMARK_POLL_INTERVAL_MS)); const pollInterval = WATERMARK_POLL_INTERVAL_MS; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { const response = await fetch( `https://techsz.aoscdn.com/api/tasks/visual/external/watermark-remove/${taskId}`, { method: "GET", headers: { "X-API-KEY": apiKey, }, } ); if (!response.ok) { const errorText = await response.text(); console.error(`[WatermarkRemove] 轮询失败 (attempt ${attempt + 1}):`, errorText); // 继续轮询 await new Promise(resolve => setTimeout(resolve, pollInterval)); continue; } const result = await response.json(); console.log(`[WatermarkRemove] 轮询结果 (attempt ${attempt + 1}):`, JSON.stringify(result).substring(0, 200)); const state = result.data?.state; const stateDetail = result.data?.state_detail; // 检查任务状态 if (state === 1) { // 任务完成 - PicWish API 返回 "file" 字段 const imageUrl = result.data?.file || result.data?.image; if (imageUrl) { return { success: true, imageUrl }; } return { success: false, error: "处理完成但未返回图片URL" }; } else if (typeof state === "number" && state < 0) { // 任务失败 return { success: false, error: result.data?.error || "去水印处理失败" }; } else if (state === 4 || state === 0) { // 明确处理中状态,继续轮询 console.log(`[WatermarkRemove] 任务处理中: state=${state}, detail=${stateDetail || "N/A"}`); } // 其他状态默认继续轮询 await new Promise(resolve => setTimeout(resolve, pollInterval)); } catch (error) { console.error(`[WatermarkRemove] 轮询错误 (attempt ${attempt + 1}):`, error); await new Promise(resolve => setTimeout(resolve, pollInterval)); } } return { success: false, pending: true, error: "去水印任务仍在处理中", }; } function buildWatermarkTaskPendingResponse(taskId: string) { return NextResponse.json( { success: true, processing: true, status: "running", taskId, pollAfterMs: WATERMARK_POLL_INTERVAL_MS, }, { status: 202 } ); } async function persistWatermarkResult(params: { processedImageUrl: string; userId: string; startedAt?: number; sourceImageUrls?: string[]; }): Promise<{ success: boolean; original_url: string; thumbnail_url: string; processing_time?: number; }> { try { const imageResponse = await fetch(params.processedImageUrl); if (!imageResponse.ok) { throw new Error("下载处理后的图片失败"); } const imageBuffer = await imageResponse.arrayBuffer(); const filename = `watermark-removed-${Date.now()}.png`; const uploadResult = await uploadFile(filename, imageBuffer); 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, thumbnailUrl: uploadResult.url, originalUrl: uploadResult.url, imageSize: "2K", prompt: "去水印处理", modelId: "watermark-remove", }).returning()); saved = true; break; } catch (error) { saveError = error; if (attempt < 3) { await new Promise((resolve) => setTimeout(resolve, 120 * attempt)); } } } if (!saved) { console.error("[WatermarkRemove] 保存到相册失败:", 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, mediaType: "image", sourceType: "edited", status: "ready", originalUrl: uploadResult.url, previewUrl: uploadResult.url, thumbnailUrl: uploadResult.url, displayName: "去水印处理", prompt: "去水印处理", modelId: "watermark-remove", 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, fromAssetId: sourceAsset.id, toAssetId: media.id, relationType: "edited_from", metadata: { prompt: "去水印处理", }, }); } } } catch (assetError) { console.error("[WatermarkRemove] MediaAsset 注册失败:", assetError); } } return { success: true, original_url: uploadResult.url, thumbnail_url: uploadResult.url, processing_time: typeof params.startedAt === "number" ? (Date.now() - params.startedAt) / 1000 : undefined, }; } catch (uploadError) { console.error("[WatermarkRemove] 上传结果失败,回退原始URL:", uploadError); return { success: true, original_url: params.processedImageUrl, thumbnail_url: params.processedImageUrl, processing_time: typeof params.startedAt === "number" ? (Date.now() - params.startedAt) / 1000 : undefined, }; } } function shouldUseFalWatermarkFallback(status: number, detail: string) { return ( status === 401 || status === 402 || status === 403 || status === 429 || status >= 500 || /quota|credit|balance|exceed|expired|invalid|unauthor/i.test(detail) ); } async function removeWatermarkWithFal(params: { imageUrl: string; userId: string; startedAt: number; sourceImageUrls?: string[]; }) { console.warn("[WatermarkRemove] AOS unavailable; using FAL Nano Banana 2 edit fallback"); const result = await generateCanvasImage({ prompt: [ "仅移除图片中后期叠加的水印、平台角标和覆盖式文字标记。", "必须完整保留原图主体、商品、背景、构图、尺寸、颜色、光影和清晰度。", "用周围真实纹理自然补全被遮挡区域,不得新增文字、标志或其他物体。", ].join(""), model: FAL_NANO_BANANA_2_EDIT_MODEL_ID, aspectRatio: "auto", imageSize: "2K", imageCount: 1, referenceImages: [params.imageUrl], allowFailover: false, debugLabel: "watermark-remove-fal-fallback", }); const processedImageUrl = result.images[0]?.originalUrl; if (!processedImageUrl) { throw new Error("FAL 去水印未返回可用图片"); } return persistWatermarkResult({ processedImageUrl, userId: params.userId, startedAt: params.startedAt, sourceImageUrls: params.sourceImageUrls, }); } async function finalizeWatermarkTaskResult(params: { taskId: string; userId: string; startedAt?: number; apiKey: string; sourceImageUrls?: string[]; }): Promise<{ success: boolean; original_url: string; thumbnail_url: string; processing_time?: number; }> { const pollResult = await pollForResult(params.taskId, params.apiKey, WATERMARK_SYNC_WAIT_TIMEOUT_MS); if (pollResult.pending) { throw new Error("WATERMARK_TASK_PENDING"); } if (!pollResult.success || !pollResult.imageUrl) { throw new Error(pollResult.error || "去水印处理失败"); } return persistWatermarkResult({ processedImageUrl: pollResult.imageUrl, userId: params.userId, startedAt: params.startedAt, sourceImageUrls: params.sourceImageUrls, }); } 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 config = await getWatermarkRemoveConfig(); if (!config?.apiKey) { return NextResponse.json( { error: "去水印服务未配置,请联系管理员在系统设置中配置API密钥" }, { status: 500 } ); } try { const result = await finalizeWatermarkTaskResult({ taskId, userId: session.user.id, apiKey: config.apiKey, }); return NextResponse.json(result); } catch (error) { if (error instanceof Error && error.message === "WATERMARK_TASK_PENDING") { return buildWatermarkTaskPendingResponse(taskId); } throw error; } } catch (error) { console.error("[WatermarkRemove][TaskStatus] 处理错误:", error); return NextResponse.json( { error: error instanceof Error ? error.message : "去水印处理失败" }, { status: 500 } ); } } export async function POST(request: NextRequest) { try { // 验证用户登录 const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "请先登录" }, { status: 401 }); } const startTime = Date.now(); // 获取请求参数 const body = await request.json(); const { image_url } = body; if (!image_url) { return NextResponse.json( { error: "请提供图片URL" }, { status: 400 } ); } // 获取API配置 const config = await getWatermarkRemoveConfig(); console.log("[WatermarkRemove] 使用原图URL:", image_url.substring(0, 100) + "..."); // 处理图片URL,确保获取公网可访问的URL let publicImageUrl: string; // 如果是 base64 格式的 data URL,先上传到 S3 if (image_url.startsWith("data:")) { console.log("[WatermarkRemove] 检测到base64格式,上传到S3..."); try { // 避免对超长 base64 使用正则,直接按 data URL 头部拆分 const commaIndex = image_url.indexOf(","); if (commaIndex <= 0) { return NextResponse.json( { error: "无效的图片格式" }, { status: 400 } ); } const header = image_url.slice(0, commaIndex); const base64Data = image_url.slice(commaIndex + 1); const headerMatch = header.match(/^data:image\/([a-zA-Z0-9.+-]+);base64$/i); if (!headerMatch || !base64Data) { return NextResponse.json( { error: "无效的图片格式" }, { status: 400 } ); } const ext = headerMatch[1].toLowerCase() === "jpeg" ? "jpg" : headerMatch[1].toLowerCase(); const buffer = Buffer.from(base64Data, "base64"); // 上传到 S3 const filename = `watermark-input-${Date.now()}.${ext}`; const uploadResult = await uploadFile(filename, buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)); publicImageUrl = uploadResult.url; console.log("[WatermarkRemove] base64图片已上传到S3:", publicImageUrl); } catch (uploadError) { console.error("[WatermarkRemove] base64上传失败:", uploadError); return NextResponse.json( { error: "图片上传失败" }, { status: 500 } ); } } // 如果是 HTTP/HTTPS URL,下载后重新上传确保公网可访问 else if (image_url.startsWith("http://") || image_url.startsWith("https://")) { try { console.log("[WatermarkRemove] 下载原图并上传到S3..."); const imageResponse = await fetch(image_url); if (!imageResponse.ok) { throw new Error("下载原图失败"); } const imageBuffer = await imageResponse.arrayBuffer(); const filename = `watermark-input-${Date.now()}.jpg`; const uploadResult = await uploadFile(filename, imageBuffer); publicImageUrl = uploadResult.url; console.log("[WatermarkRemove] 图片已上传到S3:", publicImageUrl); } catch (downloadError) { console.error("[WatermarkRemove] 下载/上传原图失败:", downloadError); // 如果下载失败,尝试使用原始URL console.log("[WatermarkRemove] 尝试使用原始URL"); publicImageUrl = image_url; } } else { return NextResponse.json( { error: "图片URL格式无效" }, { status: 400 } ); } console.log("[WatermarkRemove] 开始处理图片:", publicImageUrl); const pointsConfig = await getPointsSystemConfig(); const pointsCharge = await prepareFeaturePointsCharge({ userId: session.user.id, unitCost: getImageToolCreditCost(pointsConfig, "watermark-remove"), reason: "image_watermark_remove", modelId: "watermark-remove", note: "去水印消耗积分", pointsConfig, }); if (!pointsCharge.ok) { return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status }); } const sourceImageUrls = typeof image_url === "string" && /^https?:\/\//i.test(image_url) ? [image_url] : undefined; if (!config?.apiKey) { const fallbackResult = await removeWatermarkWithFal({ imageUrl: publicImageUrl, userId: session.user.id, startedAt: startTime, sourceImageUrls, }); if (pointsCharge.chargeable) { await pointsCharge.consume({ note: "FAL 去水印备用链路消耗积分" }); } return NextResponse.json(fallbackResult); } // 创建去水印任务 const formData = new FormData(); formData.append("sync", "0"); formData.append("url", publicImageUrl); const createResponse = await fetch( "https://techsz.aoscdn.com/api/tasks/visual/external/watermark-remove", { method: "POST", headers: { "X-API-KEY": config.apiKey, }, body: formData, } ); if (!createResponse.ok) { const errorText = await createResponse.text(); console.error("[WatermarkRemove] 创建任务失败:", errorText); if (shouldUseFalWatermarkFallback(createResponse.status, errorText)) { try { const fallbackResult = await removeWatermarkWithFal({ imageUrl: publicImageUrl, userId: session.user.id, startedAt: startTime, sourceImageUrls, }); if (pointsCharge.chargeable) { await pointsCharge.consume({ note: "FAL 去水印备用链路消耗积分" }); } return NextResponse.json(fallbackResult); } catch (fallbackError) { console.error("[WatermarkRemove] FAL 备用链路失败:", fallbackError); } } return NextResponse.json( { error: "去水印服务暂时不可用,请稍后重试" }, { status: 500 } ); } const createResult = await createResponse.json(); console.log("[WatermarkRemove] 创建任务结果:", JSON.stringify(createResult).substring(0, 200)); const taskId = createResult.data?.task_id; if (!taskId) { return NextResponse.json( { error: "未获取到任务ID" }, { status: 500 } ); } if (pointsCharge.chargeable) { try { await pointsCharge.consume({ note: `去水印任务消耗积分(${taskId})`, }); } catch (pointsError) { console.error("[Points] Failed to consume watermark remove points:", pointsError); } } try { const result = await finalizeWatermarkTaskResult({ taskId, userId: session.user.id, startedAt: startTime, apiKey: config.apiKey, sourceImageUrls, }); return NextResponse.json(result); } catch (error) { if (error instanceof Error && error.message === "WATERMARK_TASK_PENDING") { return buildWatermarkTaskPendingResponse(taskId); } throw error; } } catch (error) { console.error("[WatermarkRemove] 处理错误:", error); return NextResponse.json( { error: error instanceof Error ? error.message : "去水印处理失败" }, { status: 500 } ); } }