去水印直接复用已存七牛的图片 #69
@ -6,6 +6,7 @@ import { eq } from "drizzle-orm";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
import {
|
||||
createMediaAssetRelation,
|
||||
getDefaultStorageConfig,
|
||||
getPointsSystemConfig,
|
||||
getMediaAssetsByUserAndUrls,
|
||||
upsertMediaAsset,
|
||||
@ -21,6 +22,25 @@ export const maxDuration = 600;
|
||||
const WATERMARK_SYNC_WAIT_TIMEOUT_MS = 20_000;
|
||||
const WATERMARK_POLL_INTERVAL_MS = 1200;
|
||||
|
||||
async function resolveStoredQiniuImageUrl(source: string): Promise<string | null> {
|
||||
const config = await getDefaultStorageConfig().catch(() => null);
|
||||
if (config?.type !== "qiniu-kodo" || !config.isEnabled || !config.publicUrl) return null;
|
||||
|
||||
try {
|
||||
const base = new URL(config.publicUrl);
|
||||
const url = new URL(source);
|
||||
const prefix = base.pathname.replace(/\/$/, "") + "/";
|
||||
if (base.protocol !== "https:" || url.origin !== base.origin ||
|
||||
url.username || url.password || url.search || url.hash ||
|
||||
!url.pathname.startsWith(prefix)) return null;
|
||||
|
||||
const objectKey = url.pathname.slice(prefix.length);
|
||||
return /^uploads\/[\w.-]+$/.test(objectKey) ? url.toString() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取去水印API配置
|
||||
async function getWatermarkRemoveConfig() {
|
||||
try {
|
||||
@ -131,13 +151,16 @@ async function persistWatermarkResult(params: {
|
||||
processing_time?: number;
|
||||
}> {
|
||||
try {
|
||||
const imageResponse = await fetch(params.processedImageUrl);
|
||||
if (!imageResponse.ok) {
|
||||
throw new Error("下载处理后的图片失败");
|
||||
let resultUrl = await resolveStoredQiniuImageUrl(params.processedImageUrl);
|
||||
if (!resultUrl) {
|
||||
const imageResponse = await fetch(params.processedImageUrl);
|
||||
if (!imageResponse.ok) {
|
||||
throw new Error("下载处理后的图片失败");
|
||||
}
|
||||
const imageBuffer = await imageResponse.arrayBuffer();
|
||||
const filename = `watermark-removed-${Date.now()}.png`;
|
||||
resultUrl = (await uploadFile(filename, imageBuffer)).url;
|
||||
}
|
||||
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;
|
||||
@ -150,8 +173,8 @@ async function persistWatermarkResult(params: {
|
||||
try {
|
||||
([savedImage] = await db.insert(generatedImage).values({
|
||||
userId: params.userId,
|
||||
thumbnailUrl: uploadResult.url,
|
||||
originalUrl: uploadResult.url,
|
||||
thumbnailUrl: resultUrl,
|
||||
originalUrl: resultUrl,
|
||||
imageSize: "2K",
|
||||
prompt: "去水印处理",
|
||||
modelId: "watermark-remove",
|
||||
@ -184,9 +207,9 @@ async function persistWatermarkResult(params: {
|
||||
mediaType: "image",
|
||||
sourceType: "edited",
|
||||
status: "ready",
|
||||
originalUrl: uploadResult.url,
|
||||
previewUrl: uploadResult.url,
|
||||
thumbnailUrl: uploadResult.url,
|
||||
originalUrl: resultUrl,
|
||||
previewUrl: resultUrl,
|
||||
thumbnailUrl: resultUrl,
|
||||
displayName: "去水印处理",
|
||||
prompt: "去水印处理",
|
||||
modelId: "watermark-remove",
|
||||
@ -217,8 +240,8 @@ async function persistWatermarkResult(params: {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
original_url: uploadResult.url,
|
||||
thumbnail_url: uploadResult.url,
|
||||
original_url: resultUrl,
|
||||
thumbnail_url: resultUrl,
|
||||
processing_time:
|
||||
typeof params.startedAt === "number" ? (Date.now() - params.startedAt) / 1000 : undefined,
|
||||
};
|
||||
@ -362,7 +385,7 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const { image_url } = body;
|
||||
|
||||
if (!image_url) {
|
||||
if (typeof image_url !== "string" || !image_url) {
|
||||
return NextResponse.json(
|
||||
{ error: "请提供图片URL" },
|
||||
{ status: 400 }
|
||||
@ -413,24 +436,24 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
}
|
||||
// 如果是 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 storedImageUrl = await resolveStoredQiniuImageUrl(image_url);
|
||||
publicImageUrl = storedImageUrl || image_url;
|
||||
if (!storedImageUrl) {
|
||||
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`;
|
||||
publicImageUrl = (await uploadFile(filename, imageBuffer)).url;
|
||||
console.log("[WatermarkRemove] 图片已上传到S3:", publicImageUrl);
|
||||
} catch (downloadError) {
|
||||
console.error("[WatermarkRemove] 下载/上传原图失败,使用原始URL:", downloadError);
|
||||
}
|
||||
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(
|
||||
|
||||
@ -7,6 +7,65 @@ import { serverLoader } from "../helpers/canvas-download-server-loader";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
test.describe("Qiniu image delivery", () => {
|
||||
for (const source of [
|
||||
"https://cdn.example/media/uploads/source.png",
|
||||
"https://cdn.example/media/uploads/source.png?token=temporary",
|
||||
"https://cdn.example/media-other/uploads/source.png",
|
||||
]) {
|
||||
test(`watermark removal handles ${source} according to its storage origin and signature`, async () => {
|
||||
const storedResult = "https://cdn.example/media/uploads/result.png";
|
||||
const uploadedInput = "https://cdn.example/media/uploads/staged.png";
|
||||
const reusesSource = source === "https://cdn.example/media/uploads/source.png";
|
||||
const transferred: string[] = [];
|
||||
const uploaded: string[] = [];
|
||||
const saved: string[] = [];
|
||||
const route = serverLoader({
|
||||
"next/server": { NextResponse: Response },
|
||||
"@/app/(auth)/auth": { auth: async () => ({ user: { id: "user-1" } }) },
|
||||
"@/lib/db": { db: {
|
||||
select: () => ({ from: () => ({ where: () => ({ limit: async () => [{ value: { apiKey: "fixture" } }] }) }) }),
|
||||
insert: () => ({ values: (row: { originalUrl: string }) => ({
|
||||
returning: async () => { saved.push(row.originalUrl); return [{ id: "image-1" }]; },
|
||||
}) }),
|
||||
} },
|
||||
"@/lib/db/schema": { systemSettings: {}, generatedImage: {} },
|
||||
"drizzle-orm": { eq: () => null },
|
||||
"@/lib/db/queries": {
|
||||
getDefaultStorageConfig: async () => ({ type: "qiniu-kodo", isEnabled: true, publicUrl: "https://cdn.example/media" }),
|
||||
getPointsSystemConfig: async () => ({}),
|
||||
getMediaAssetsByUserAndUrls: async () => [],
|
||||
upsertMediaAsset: async () => null,
|
||||
},
|
||||
"@/lib/storage/service": {
|
||||
uploadFile: async (filename: string) => { uploaded.push(filename); return { url: uploadedInput }; },
|
||||
},
|
||||
"@/lib/points/feature-charge": { prepareFeaturePointsCharge: async () => ({ ok: true, chargeable: false }) },
|
||||
"@/lib/points/config": { getImageToolCreditCost: () => 0 },
|
||||
"@/lib/canvas/generate-image": {},
|
||||
fetch: async (url: string, init?: RequestInit) => {
|
||||
if (new URL(url).hostname === "techsz.aoscdn.com") {
|
||||
if (init?.method === "POST") {
|
||||
expect((init.body as FormData).get("url")).toBe(reusesSource ? source : uploadedInput);
|
||||
return Response.json({ data: { task_id: "fixture-task" } });
|
||||
}
|
||||
return Response.json({ data: { state: 1, file: storedResult } });
|
||||
}
|
||||
transferred.push(url);
|
||||
return new Response(new Uint8Array([1, 2, 3]));
|
||||
},
|
||||
})("app/api/watermark-remove/route.ts");
|
||||
|
||||
const response = await route.POST(new Request("https://app.example/api/watermark-remove", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ image_url: source }),
|
||||
}));
|
||||
expect(await response.json()).toMatchObject({ success: true, original_url: storedResult });
|
||||
expect(transferred).toEqual(reusesSource ? [] : [source]);
|
||||
expect(uploaded).toHaveLength(reusesSource ? 0 : 1);
|
||||
expect(saved).toEqual([storedResult]);
|
||||
});
|
||||
}
|
||||
|
||||
test("model result buckets use our thumbnail endpoint without forbidden OSS transforms", () => {
|
||||
const source = "https://dashscope-a717.oss-accelerate.aliyuncs.com/output.png";
|
||||
const preview = new URL(buildImageProxyUrl(source, { width: 960, format: "webp" }), "https://app.example");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user