diff --git a/app/api/extend/route.ts b/app/api/extend/route.ts index f5a6fc0..9c295e1 100644 --- a/app/api/extend/route.ts +++ b/app/api/extend/route.ts @@ -85,26 +85,61 @@ async function uploadBase64ToS3(base64Data: string, mimeType: string = "image/jp export const maxDuration = 180; /** - * 图像拓展核心实现 - * @param imageBase64 - Base64编码的图片数据 + * 将图片上传到S3并返回公网URL(用于API调用) + */ +async function uploadImageToS3ForAPI(imageBase64: string, mimeType: string = "image/jpeg"): Promise { + try { + // 解析 base64 数据 + const buffer = Buffer.from(imageBase64, "base64"); + + // 生成文件名 + const ext = mimeType.split("/")[1] || "jpg"; + const timestamp = Date.now(); + const randomStr = Math.random().toString(36).slice(2, 8); + const filename = `extend-input-${timestamp}-${randomStr}.${ext}`; + + console.log(`[Extend API] 上传原图到S3,大小: ${(buffer.length / 1024).toFixed(2)}KB`); + + // 转换为 ArrayBuffer + const arrayBuffer = buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength + ) as ArrayBuffer; + + // 上传到 S3 + const result = await uploadFile(filename, arrayBuffer); + + console.log(`[Extend API] ✅ 原图已上传到S3: ${result.url}`); + + return result.url; + } catch (error) { + console.error("[Extend API] 上传到S3失败:", error); + throw new Error(`图片上传到存储桶失败: ${error instanceof Error ? error.message : "未知错误"}。请检查系统设置中的存储配置。`); + } +} + +/** + * 图像拓展核心实现(使用URL方式,避免Base64长度限制) + * @param imageUrl - 图片的公网URL * @param apiKey - Dashscope API Key * @param xScale - X轴放大倍数(默认1.5) * @param yScale - Y轴放大倍数(默认1.5) * @returns 拓展后的图片URL */ async function extendImage( - imageBase64: string, + imageUrl: string, apiKey: string, xScale: number = 1.5, yScale: number = 1.5 ): Promise { console.log("[Extend API] 开始图像拓展..."); + console.log("[Extend API] 使用图片URL:", imageUrl); - // 1. 提交拓展任务 + // 1. 提交拓展任务(使用URL方式,避免Base64长度限制) const requestBody = { model: "image-out-painting", input: { - image_base64: imageBase64, + image_url: imageUrl, }, parameters: { x_scale: xScale, @@ -187,6 +222,7 @@ async function extendImage( throw new Error(`图像拓展任务超时(等待${maxAttempts * 2 + 3}秒)`); } + /** * 图像拓展API路由 * 从数据库读取图像拓展API配置,调用Dashscope API进行图像拓展处理 @@ -286,14 +322,19 @@ export async function POST(request: NextRequest) { console.log(`[Extend API] 图片Base64长度: ${imageBase64.length} 字符`); - // 3. 调用图像拓展API + // 3. 先将图片上传到S3获取公网URL(使用URL方式避免Base64长度限制) + console.log("[Extend API] 上传原图到S3获取公网URL..."); + const inputImageUrl = await uploadImageToS3ForAPI(imageBase64, "image/jpeg"); + console.log("[Extend API] 原图URL:", inputImageUrl); + + // 4. 调用图像拓展API(使用URL方式) const startTime = Date.now(); const xScale = config.xScale || 1.5; const yScale = config.yScale || 1.5; console.log(`[Extend API] 拓展参数: X=${xScale}, Y=${yScale}`); - const extendedImageUrl = await extendImage(imageBase64, config.apiKey, xScale, yScale); + const extendedImageUrl = await extendImage(inputImageUrl, config.apiKey, xScale, yScale); console.log("[Extend API] 拓展后图片URL:", extendedImageUrl); // 4. 下载拓展后的图片 @@ -350,3 +391,4 @@ export async function POST(request: NextRequest) { } } + diff --git a/components/chat.tsx b/components/chat.tsx index 3d72c34..c627a4f 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -111,6 +111,10 @@ export function Chat({ const [usage, setUsage] = useState(initialLastContext); const [showCreditCardAlert, setShowCreditCardAlert] = useState(false); const [currentModelId, setCurrentModelId] = useState(initialChatModel); + // 图片处理状态,防止重复点击 + const [isProcessingEnhance, setIsProcessingEnhance] = useState(false); + const [isProcessingRmbg, setIsProcessingRmbg] = useState(false); + const [isProcessingExtend, setIsProcessingExtend] = useState(false); const currentModelIdRef = useRef(currentModelId); // 🆕 视频参数状态 @@ -502,6 +506,13 @@ export function Chat({ // 处理图片变清晰 const handleImageEnhance = async (imageUrl: string) => { + // 防止重复点击 + if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend) { + toast.info("正在处理中,请稍候..."); + return; + } + + setIsProcessingEnhance(true); const messageId = generateUUID(); // 创建临时加载消息 @@ -514,6 +525,17 @@ export function Chat({ setMessages((prev) => [...prev, loadingMessage]); + // 延迟滚动到底部,确保DOM已更新 + setTimeout(() => { + const messagesContainer = document.querySelector('[role="log"]'); + if (messagesContainer) { + messagesContainer.scrollTo({ + top: messagesContainer.scrollHeight, + behavior: "smooth", + }); + } + }, 100); + try { // 获取图片并转换为Base64 const response = await fetch(imageUrl); @@ -562,6 +584,17 @@ export function Chat({ prev.map((msg) => (msg.id === messageId ? finalMessage : msg)) ); + // 滚动到底部,显示处理结果 + setTimeout(() => { + const messagesContainer = document.querySelector('[role="log"]'); + if (messagesContainer) { + messagesContainer.scrollTo({ + top: messagesContainer.scrollHeight, + behavior: "smooth", + }); + } + }, 100); + // 保存消息到数据库 try { await fetch("/api/messages", { @@ -593,12 +626,15 @@ export function Chat({ toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试"); // 移除加载消息 setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); + } finally { + setIsProcessingEnhance(false); } }; reader.onerror = () => { toast.error("图片读取失败"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); + setIsProcessingEnhance(false); }; reader.readAsDataURL(blob); @@ -606,11 +642,19 @@ export function Chat({ console.error("[变清晰] 错误:", error); toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); + setIsProcessingEnhance(false); } }; // 处理图片抠图 const handleImageRmbg = async (imageUrl: string) => { + // 防止重复点击 + if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend) { + toast.info("正在处理中,请稍候..."); + return; + } + + setIsProcessingRmbg(true); const messageId = generateUUID(); // 创建临时加载消息 @@ -623,6 +667,17 @@ export function Chat({ setMessages((prev) => [...prev, loadingMessage]); + // 延迟滚动到底部,确保DOM已更新 + setTimeout(() => { + const messagesContainer = document.querySelector('[role="log"]'); + if (messagesContainer) { + messagesContainer.scrollTo({ + top: messagesContainer.scrollHeight, + behavior: "smooth", + }); + } + }, 100); + try { // 获取图片并转换为Base64 const response = await fetch(imageUrl); @@ -671,6 +726,17 @@ export function Chat({ prev.map((msg) => (msg.id === messageId ? finalMessage : msg)) ); + // 滚动到底部,显示处理结果 + setTimeout(() => { + const messagesContainer = document.querySelector('[role="log"]'); + if (messagesContainer) { + messagesContainer.scrollTo({ + top: messagesContainer.scrollHeight, + behavior: "smooth", + }); + } + }, 100); + // 保存消息到数据库 try { await fetch("/api/messages", { @@ -702,12 +768,15 @@ export function Chat({ toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试"); // 移除加载消息 setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); + } finally { + setIsProcessingRmbg(false); } }; reader.onerror = () => { toast.error("图片读取失败"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); + setIsProcessingRmbg(false); }; reader.readAsDataURL(blob); @@ -715,11 +784,19 @@ export function Chat({ console.error("[抠图] 错误:", error); toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); + setIsProcessingRmbg(false); } }; // 处理图像拓展 const handleImageExtend = async (imageUrl: string) => { + // 防止重复点击 + if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend) { + toast.info("正在处理中,请稍候..."); + return; + } + + setIsProcessingExtend(true); const messageId = generateUUID(); // 创建临时加载消息 @@ -734,6 +811,17 @@ export function Chat({ setMessages((prev) => [...prev, loadingMessage]); + // 延迟滚动到底部,确保DOM已更新 + setTimeout(() => { + const messagesContainer = document.querySelector('[role="log"]'); + if (messagesContainer) { + messagesContainer.scrollTo({ + top: messagesContainer.scrollHeight, + behavior: "smooth", + }); + } + }, 100); + try { // 获取图片并转换为Base64 const response = await fetch(imageUrl); @@ -782,6 +870,17 @@ export function Chat({ prev.map((msg) => (msg.id === messageId ? finalMessage : msg)) ); + // 滚动到底部,显示处理结果 + setTimeout(() => { + const messagesContainer = document.querySelector('[role="log"]'); + if (messagesContainer) { + messagesContainer.scrollTo({ + top: messagesContainer.scrollHeight, + behavior: "smooth", + }); + } + }, 100); + // 保存消息到数据库 try { await fetch("/api/messages", { @@ -813,12 +912,15 @@ export function Chat({ toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试"); // 移除加载消息 setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); + } finally { + setIsProcessingExtend(false); } }; reader.onerror = () => { toast.error("图片读取失败"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); + setIsProcessingExtend(false); }; reader.readAsDataURL(blob); @@ -826,6 +928,7 @@ export function Chat({ console.error("[图像拓展] 错误:", error); toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); + setIsProcessingExtend(false); } }; @@ -861,6 +964,9 @@ export function Chat({ onImageEnhance={handleImageEnhance} onImageRmbg={handleImageRmbg} onImageExtend={handleImageExtend} + isProcessingEnhance={isProcessingEnhance} + isProcessingRmbg={isProcessingRmbg} + isProcessingExtend={isProcessingExtend} pendingGeneration={pendingGeneration} imageCount={imageParams?.imageCount || 1} /> diff --git a/components/elements/response.tsx b/components/elements/response.tsx index f2b2cd5..f4a4a7e 100644 --- a/components/elements/response.tsx +++ b/components/elements/response.tsx @@ -12,10 +12,13 @@ type ResponseProps = ComponentProps & { onImageEnhance?: (imageUrl: string) => void; onImageRmbg?: (imageUrl: string) => void; onImageExtend?: (imageUrl: string) => void; + isProcessingEnhance?: boolean; + isProcessingRmbg?: boolean; + isProcessingExtend?: boolean; }; export const Response = memo( - ({ className, onImageEdit, onDrawingComplete, onImageEnhance, onImageRmbg, onImageExtend, ...props }: ResponseProps) => ( + ({ className, onImageEdit, onDrawingComplete, onImageEnhance, onImageRmbg, onImageExtend, isProcessingEnhance, isProcessingRmbg, isProcessingExtend, ...props }: ResponseProps) => ( *:first-child]:mt-0 [&>*:last-child]:mb-0 [&_code]:whitespace-pre-wrap [&_code]:break-words [&_pre]:max-w-full [&_pre]:overflow-x-auto", @@ -34,6 +37,9 @@ export const Response = memo( onImageEnhance={onImageEnhance} onImageRmbg={onImageRmbg} onImageExtend={onImageExtend} + isProcessingEnhance={isProcessingEnhance} + isProcessingRmbg={isProcessingRmbg} + isProcessingExtend={isProcessingExtend} />; }, video: ({ src, alt }: { src?: string; alt?: string }) => { @@ -52,7 +58,10 @@ export const Response = memo( prevProps.onDrawingComplete === nextProps.onDrawingComplete && prevProps.onImageEnhance === nextProps.onImageEnhance && prevProps.onImageRmbg === nextProps.onImageRmbg && - prevProps.onImageExtend === nextProps.onImageExtend + prevProps.onImageExtend === nextProps.onImageExtend && + prevProps.isProcessingEnhance === nextProps.isProcessingEnhance && + prevProps.isProcessingRmbg === nextProps.isProcessingRmbg && + prevProps.isProcessingExtend === nextProps.isProcessingExtend ); Response.displayName = "Response"; diff --git a/components/image-preview.tsx b/components/image-preview.tsx index 58ee38c..fb60d69 100644 --- a/components/image-preview.tsx +++ b/components/image-preview.tsx @@ -14,6 +14,9 @@ export function ImagePreview({ onImageEnhance, onImageRmbg, onImageExtend, + isProcessingEnhance, + isProcessingRmbg, + isProcessingExtend, imageIndex }: { src: string; @@ -28,6 +31,9 @@ export function ImagePreview({ onImageEnhance?: (imageUrl: string) => void; onImageRmbg?: (imageUrl: string) => void; onImageExtend?: (imageUrl: string) => void; + isProcessingEnhance?: boolean; + isProcessingRmbg?: boolean; + isProcessingExtend?: boolean; imageIndex?: number; }) { const [isOpen, setIsOpen] = useState(false); @@ -226,11 +232,14 @@ export function ImagePreview({
{onImageEnhance && (