From f2d7aff7c60d2c9ef9eeaa2d2ec129f881734281 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 3 Feb 2026 10:32:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=8E=BB=E6=B0=B4?= =?UTF-8?q?=E5=8D=B0=E5=8A=9F=E8=83=BD=20-=20=E5=9B=BE=E7=89=87=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=A0=8F=E6=96=B0=E5=A2=9E=E5=8E=BB=E6=B0=B4=E5=8D=B0?= =?UTF-8?q?=E6=8C=89=E9=92=AE=EF=BC=8C=E7=B3=BB=E7=BB=9F=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E6=96=B0=E5=A2=9EPicWish=20API=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/admin-settings-client.tsx | 87 +++++++ app/api/watermark-remove/route.ts | 219 ++++++++++++++++++ components/chat.tsx | 107 +++++++++ components/elements/response.tsx | 8 +- components/image-preview.tsx | 31 ++- components/message.tsx | 6 + components/messages.tsx | 6 + 7 files changed, 459 insertions(+), 5 deletions(-) create mode 100644 app/api/watermark-remove/route.ts diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index df4d76c0e..02e386159 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -447,6 +447,11 @@ export function AdminSettingsClient() { yScale: "1.5", // Y轴放大倍数 }); + // 去水印API配置状态 + const [watermarkRemoveConfig, setWatermarkRemoveConfig] = useState({ + apiKey: "", // PicWish API密钥 + }); + // 角度编辑API配置状态 const [angleEditConfig, setAngleEditConfig] = useState({ enabled: false, @@ -531,6 +536,19 @@ export function AdminSettingsClient() { } }, [extendConfigData]); + // 加载去水印API配置 + const { data: watermarkRemoveConfigData } = useSWR("/api/admin/settings?key=watermark_remove_config", fetcher); + useEffect(() => { + if (watermarkRemoveConfigData?.setting?.value) { + const config = watermarkRemoveConfigData.setting.value as { + apiKey?: string; + }; + setWatermarkRemoveConfig({ + apiKey: config.apiKey || "", + }); + } + }, [watermarkRemoveConfigData]); + // 加载角度编辑API配置 const { data: angleEditConfigData } = useSWR("/api/admin/settings?key=angle_edit_config", fetcher); useEffect(() => { @@ -1013,6 +1031,32 @@ export function AdminSettingsClient() { } }; + // --- 去水印API配置逻辑 --- + const handleSaveWatermarkRemoveConfig = async () => { + try { + if (!watermarkRemoveConfig.apiKey.trim()) { + toast.error("请输入PicWish API密钥"); + return; + } + + const response = await fetch("/api/admin/settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + key: "watermark_remove_config", + value: { + apiKey: watermarkRemoveConfig.apiKey.trim(), + }, + }), + }); + + if (!response.ok) throw new Error("保存失败"); + toast.success("去水印API配置保存成功"); + } catch (error) { + toast.error("保存去水印API配置失败"); + } + }; + // --- 角度编辑API配置逻辑 --- const handleSaveAngleEditConfig = async () => { try { @@ -4559,6 +4603,49 @@ export function AdminSettingsClient() { + {/* 去水印API配置 */} + + + + + + + + 去水印API配置 + + + 配置PicWish去水印API,用于自动去除图片中的水印 + + + +
+ + setWatermarkRemoveConfig({ ...watermarkRemoveConfig, apiKey: e.target.value })} + placeholder="输入PicWish API密钥" + /> +
+ 在 PicWish 开发者平台获取API密钥 +
+
+
+
PicWish API 说明
+
+

• 使用 PicWish 云服务进行智能去水印

+

• 支持各种类型的水印去除(文字、图片水印)

+

• API文档:https://picwish.com/developer

+

• 处理时间:约10-30秒/张

+
+
+ +
+
+ {/* 角度编辑API配置 */} diff --git a/app/api/watermark-remove/route.ts b/app/api/watermark-remove/route.ts new file mode 100644 index 000000000..0f632c2b4 --- /dev/null +++ b/app/api/watermark-remove/route.ts @@ -0,0 +1,219 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/app/(auth)/auth"; +import { db } from "@/lib/db"; +import { systemSettings } from "@/lib/db/schema"; +import { eq } from "drizzle-orm"; +import { uploadFile } from "@/lib/storage/service"; + +// 获取去水印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): Promise<{ success: boolean; imageUrl?: string; error?: string }> { + const maxAttempts = 30; // 最多轮询30次(30秒) + const pollInterval = 1000; // 每次间隔1秒 + + 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)); + + // 检查任务状态 + if (result.data?.state === 1) { + // 任务完成 + const imageUrl = result.data?.image; + if (imageUrl) { + return { success: true, imageUrl }; + } + return { success: false, error: "处理完成但未返回图片URL" }; + } else if (result.data?.state === -1) { + // 任务失败 + return { success: false, error: result.data?.error || "去水印处理失败" }; + } + // state === 0 表示处理中,继续轮询 + + 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, error: "处理超时,请稍后重试" }; +} + +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, image_base64 } = body; + + if (!image_url && !image_base64) { + return NextResponse.json( + { error: "请提供图片URL或Base64数据" }, + { status: 400 } + ); + } + + // 获取API配置 + const config = await getWatermarkRemoveConfig(); + if (!config?.apiKey) { + return NextResponse.json( + { error: "去水印服务未配置,请联系管理员在系统设置中配置API密钥" }, + { status: 500 } + ); + } + + // 如果传入的是base64,需要先上传到S3获取URL + let imageUrl = image_url; + if (image_base64 && !image_url) { + try { + const buffer = Buffer.from(image_base64, "base64"); + const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); + const filename = `watermark-temp-${Date.now()}.jpeg`; + const uploadResult = await uploadFile(filename, arrayBuffer); + imageUrl = uploadResult.url; + console.log("[WatermarkRemove] Base64图片已上传到S3:", imageUrl); + } catch (uploadError) { + console.error("[WatermarkRemove] 上传图片失败:", uploadError); + return NextResponse.json( + { error: "上传图片失败" }, + { status: 500 } + ); + } + } + + console.log("[WatermarkRemove] 开始处理图片:", imageUrl); + + // 创建去水印任务 + const formData = new FormData(); + formData.append("sync", "0"); + formData.append("url", imageUrl); + + 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); + 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 } + ); + } + + // 轮询获取结果 + const pollResult = await pollForResult(taskId, config.apiKey); + + if (!pollResult.success) { + return NextResponse.json( + { error: pollResult.error || "去水印处理失败" }, + { status: 500 } + ); + } + + // 下载处理后的图片并上传到S3 + const processedImageUrl = pollResult.imageUrl!; + console.log("[WatermarkRemove] 处理完成,结果URL:", processedImageUrl); + + try { + // 下载处理后的图片 + const imageResponse = await fetch(processedImageUrl); + if (!imageResponse.ok) { + throw new Error("下载处理后的图片失败"); + } + const imageBuffer = await imageResponse.arrayBuffer(); + + // 上传到S3 + const filename = `watermark-removed-${Date.now()}.png`; + const uploadResult = await uploadFile(filename, imageBuffer); + + const processingTime = (Date.now() - startTime) / 1000; + console.log(`[WatermarkRemove] 处理完成,耗时: ${processingTime}s`); + + return NextResponse.json({ + success: true, + original_url: uploadResult.url, + thumbnail_url: uploadResult.url, + processing_time: processingTime, + }); + } catch (uploadError) { + console.error("[WatermarkRemove] 上传结果失败:", uploadError); + // 如果上传失败,直接返回原始URL + const processingTime = (Date.now() - startTime) / 1000; + return NextResponse.json({ + success: true, + original_url: processedImageUrl, + thumbnail_url: processedImageUrl, + processing_time: processingTime, + }); + } + } catch (error) { + console.error("[WatermarkRemove] 处理错误:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "去水印处理失败" }, + { status: 500 } + ); + } +} diff --git a/components/chat.tsx b/components/chat.tsx index 2469e4b6e..67c81865b 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -123,6 +123,7 @@ export function Chat({ const [isProcessingRmbg, setIsProcessingRmbg] = useState(false); const [isProcessingExtend, setIsProcessingExtend] = useState(false); const [isProcessingAngleEdit, setIsProcessingAngleEdit] = useState(false); + const [isProcessingWatermarkRemove, setIsProcessingWatermarkRemove] = useState(false); const currentModelIdRef = useRef(currentModelId); // 🆕 视频参数状态 @@ -889,6 +890,110 @@ export function Chat({ } }; + // 处理图片去水印 + const handleImageWatermarkRemove = async (imageUrl: string) => { + // 防止重复点击 + if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend || isProcessingWatermarkRemove) { + toast.info("正在处理中,请稍候..."); + return; + } + + setIsProcessingWatermarkRemove(true); + const messageId = generateUUID(); + + // 创建临时加载消息 + const loadingMessage: ChatMessage = { + id: messageId, + role: "assistant", + parts: [{ type: "text", text: "正在去水印..." }], + createdAt: new Date().toISOString(), + } as ChatMessage; + + setMessages((prev) => [...prev, loadingMessage]); + + // 延迟滚动到底部,确保DOM已更新 + setTimeout(() => { + const messagesContainer = document.querySelector('[role="log"]'); + if (messagesContainer) { + messagesContainer.scrollTo({ + top: messagesContainer.scrollHeight, + behavior: "smooth", + }); + } + }, 100); + + try { + // 调用去水印API + const watermarkRemoveResponse = await fetch("/api/watermark-remove", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + image_url: imageUrl, + }), + }); + + if (!watermarkRemoveResponse.ok) { + const error = await watermarkRemoveResponse.json(); + throw new Error(error.error || "去水印处理失败"); + } + + const result = await watermarkRemoveResponse.json(); + + if (result.success && result.original_url) { + // 构造图片Markdown(使用与Gemini生成相同的格式) + const imageMarkdown = `![thumb|||${result.original_url}|||2K](${result.thumbnail_url || result.original_url})`; + + // 创建最终消息(前端显示用) + const finalMessage: ChatMessage = { + id: messageId, + role: "assistant", + parts: [{ type: "text", text: imageMarkdown }], + createdAt: new Date().toISOString(), + } as ChatMessage; + + setMessages((prev) => { + const filtered = prev.filter((msg) => msg.id !== messageId); + return [...filtered, finalMessage]; + }); + + // 保存消息到数据库 + try { + const saveResponse = await fetch("/api/messages", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chatId: id, + message: { + id: messageId, + role: "assistant", + parts: [{ type: "text", text: imageMarkdown }], + }, + }), + }); + + if (!saveResponse.ok) { + console.error("[Chat] Failed to save message:", await saveResponse.text()); + } + } catch (saveError) { + console.error("[Chat] Failed to save message:", saveError); + } + + toast.success(`去水印完成!处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`); + } else { + throw new Error(result.error || "去水印处理失败"); + } + } catch (error) { + console.error("[去水印] 错误:", error); + toast.error(error instanceof Error ? error.message : "去水印失败,请稍后重试"); + // 移除加载消息 + setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); + } finally { + setIsProcessingWatermarkRemove(false); + } + }; + // 处理图像拓展 const handleImageExtend = async (imageUrl: string) => { // 防止重复点击 @@ -1157,10 +1262,12 @@ export function Chat({ onImageRmbg={handleImageRmbg} onImageExtend={handleImageExtend} onImageAngleEdit={handleImageAngleEdit} + onImageWatermarkRemove={handleImageWatermarkRemove} isProcessingEnhance={isProcessingEnhance} isProcessingRmbg={isProcessingRmbg} isProcessingExtend={isProcessingExtend} isProcessingAngleEdit={isProcessingAngleEdit} + isProcessingWatermarkRemove={isProcessingWatermarkRemove} pendingGeneration={pendingGeneration} imageCount={imageParams?.imageCount || 1} isGeneratingImageFlag={isGeneratingImageFlag} diff --git a/components/elements/response.tsx b/components/elements/response.tsx index 14cea4532..6e6d314b2 100644 --- a/components/elements/response.tsx +++ b/components/elements/response.tsx @@ -13,10 +13,12 @@ type ResponseProps = ComponentProps & { onImageRmbg?: (imageUrl: string) => void; onImageExtend?: (imageUrl: string) => void; onImageAngleEdit?: (imageUrl: string) => void; + onImageWatermarkRemove?: (imageUrl: string) => void; isProcessingEnhance?: boolean; isProcessingRmbg?: boolean; isProcessingExtend?: boolean; isProcessingAngleEdit?: boolean; + isProcessingWatermarkRemove?: boolean; }; // 注意:移除了 memo 的自定义比较函数,确保 isProcessing* 状态变化时组件能正确重新渲染 @@ -29,10 +31,12 @@ export const Response = ({ onImageRmbg, onImageExtend, onImageAngleEdit, + onImageWatermarkRemove, isProcessingEnhance, isProcessingRmbg, isProcessingExtend, isProcessingAngleEdit, + isProcessingWatermarkRemove, ...props }: ResponseProps) => ( ; }, video: ({ src, alt }: { src?: string; alt?: string }) => { diff --git a/components/image-preview.tsx b/components/image-preview.tsx index 66fd1d395..45275de98 100644 --- a/components/image-preview.tsx +++ b/components/image-preview.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState, useEffect } from "react"; -import { X, Download, Edit, Target, Paintbrush, Sparkles, Scissors, Expand, Maximize2, Move3D } from "lucide-react"; +import { X, Download, Edit, Target, Paintbrush, Sparkles, Scissors, Expand, Maximize2, Move3D, Droplet } from "lucide-react"; import { toast } from "sonner"; import { ImageAnnotator } from "./image-annotator"; import { AngleEditDialog } from "./angle-edit-dialog"; @@ -16,10 +16,12 @@ export function ImagePreview({ onImageRmbg, onImageExtend, onImageAngleEdit, + onImageWatermarkRemove, isProcessingEnhance, isProcessingRmbg, isProcessingExtend, isProcessingAngleEdit, + isProcessingWatermarkRemove, imageIndex }: { src: string; @@ -35,10 +37,12 @@ export function ImagePreview({ onImageRmbg?: (imageUrl: string) => void; onImageExtend?: (imageUrl: string) => void; onImageAngleEdit?: (imageUrl: string) => void; + onImageWatermarkRemove?: (imageUrl: string) => void; isProcessingEnhance?: boolean; isProcessingRmbg?: boolean; isProcessingExtend?: boolean; isProcessingAngleEdit?: boolean; + isProcessingWatermarkRemove?: boolean; imageIndex?: number; }) { const [isOpen, setIsOpen] = useState(false); @@ -55,9 +59,10 @@ export function ImagePreview({ isProcessingRmbg, isProcessingExtend, isProcessingAngleEdit, + isProcessingWatermarkRemove, src: src?.substring(0, 50) }); - }, [isProcessingEnhance, isProcessingRmbg, isProcessingExtend, isProcessingAngleEdit, src]); + }, [isProcessingEnhance, isProcessingRmbg, isProcessingExtend, isProcessingAngleEdit, isProcessingWatermarkRemove, src]); // 解析 alt 文本,提取原图 URL 和宽度 // 格式优先级: @@ -326,11 +331,11 @@ export function ImagePreview({ className="flex items-center gap-1 rounded-full bg-black/70 hover:bg-black/80 px-2 py-1 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap" onClick={(e) => { e.stopPropagation(); - if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend && !isProcessingAngleEdit) { + if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend && !isProcessingAngleEdit && !isProcessingWatermarkRemove) { setShowAngleEditDialog(true); } }} - disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend || isProcessingAngleEdit} + disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend || isProcessingAngleEdit || isProcessingWatermarkRemove} type="button" aria-label="角度调整" title="角度调整" @@ -339,6 +344,24 @@ export function ImagePreview({ 角度调整 )} + {onImageWatermarkRemove && ( + + )}