560 lines
18 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 }
);
}
}