diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 455693b9a..babf53682 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -939,6 +939,11 @@ export async function POST(request: Request) { .join(" "); autoTextForFallback = textContent; const autoContextText = typeof autoContext === "string" ? autoContext.trim() : ""; + const hasSelectedImageHint = /用户已选中\s*\d+\s*张图片/.test(autoContextText); + const hasExplicitTargetHint = /第[一二三四五六七八九十\d]+张|左|右|上|下|中间|选中/.test(textContent); + const hasActionableEditIntent = /改|调整|优化|换|替换|去掉|抠|重绘|增强|修|处理|按照|按|保留|统一|色差|背景|清晰|风格/.test( + textContent + ); // @ts-ignore - 检查各种可能的图片类型 let hasImage = lastUserMessage.parts.some( @@ -1084,6 +1089,29 @@ export async function POST(request: Request) { smartAutoDetectedIntent = autoGraphResult.analysis.intent; smartAutoNeedsClarification = Boolean(autoGraphResult.analysis.needsClarification); smartAutoClarificationQuestion = autoGraphResult.analysis.clarifyingQuestions?.[0]; + + if ( + smartAutoNeedsClarification && + hasImage && + hasActionableEditIntent && + (hasSelectedImageHint || hasExplicitTargetHint) + ) { + smartAutoNeedsClarification = false; + smartAutoClarificationQuestion = undefined; + if (!smartAutoOptimizedPrompt && textContent.trim()) { + smartAutoOptimizedPrompt = textContent.trim(); + } + if ( + !smartAutoFriendlyMessage || + /[??]$/.test(smartAutoFriendlyMessage.trim()) || + /哪一张|哪个|请先确认/.test(smartAutoFriendlyMessage) + ) { + smartAutoFriendlyMessage = "收到,我将按你指定的目标图直接执行。"; + } + console.log( + "[Smart Auto Mode] ✅ Skip clarification due to explicit target/selection context" + ); + } } if (autoGraphResult.friendlyMessage) { @@ -1091,6 +1119,19 @@ export async function POST(request: Request) { console.log("[Smart Auto Mode] 💬 Conversational reply:", smartAutoFriendlyMessage); } + if ( + !smartAutoNeedsClarification && + hasImage && + hasActionableEditIntent && + (hasSelectedImageHint || hasExplicitTargetHint) && + smartAutoFriendlyMessage && + (/[??]$/.test(smartAutoFriendlyMessage.trim()) || + /哪一张|哪个|请先确认/.test(smartAutoFriendlyMessage)) + ) { + smartAutoFriendlyMessage = "收到,我将按你指定的目标图直接执行。"; + console.log("[Smart Auto Mode] ✅ Replace clarification-like friendly reply"); + } + const autoIntent = autoGraphResult.analysis?.intent; const isImageTask = autoIntent === "image_gen" || diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 6c078402f..8279fa71e 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -1260,7 +1260,29 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF }); // 添加文本部分(用户可见) - let textContent = input.trim() || (effectiveAttachments.length > 0 ? "请处理这张图片" : ""); + let textContent = input + .replace(/\u200b/g, " ") + .replace(/\s+/g, " ") + .trim(); + + if (textContent && effectiveAttachments.length > 0) { + let strippedText = textContent; + for (const attachment of effectiveAttachments) { + const fullName = (attachment.name || "").trim(); + const shortName = fullName.length > 10 ? `${fullName.substring(0, 8)}...` : fullName; + for (const candidate of [fullName, shortName]) { + if (!candidate) continue; + if (strippedText.startsWith(candidate)) { + strippedText = strippedText.slice(candidate.length).trimStart(); + } + } + } + textContent = strippedText || textContent; + } + + if (!textContent && effectiveAttachments.length > 0) { + textContent = "请处理这张图片"; + } // 🆕 隐藏提示词(不在 UI 中展示) const hiddenSegments: string[] = []; @@ -1968,16 +1990,15 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF className="min-h-[40px] max-h-[120px] overflow-y-auto text-sm outline-none empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground/50" data-placeholder="请输入你的设计需求,例如:绿色椅子 替换 Image" onInput={(e) => { - // 从 contenteditable 中提取纯文本(排除图片标签的 data-url) + // 从 contenteditable 中提取纯文本(排除附件标签文本) const target = e.target as HTMLDivElement; - // 获取所有文本节点的内容 - let textContent = ''; - const walker = document.createTreeWalker(target, NodeFilter.SHOW_TEXT, null); - let node; - while (node = walker.nextNode()) { - textContent += node.textContent; - } - setInput(textContent.trim()); + const clone = target.cloneNode(true) as HTMLDivElement; + clone.querySelectorAll("[data-attachment-url]").forEach((el) => el.remove()); + const textContent = (clone.textContent || "") + .replace(/\u200b/g, " ") + .replace(/\s+/g, " ") + .trim(); + setInput(textContent); syncAttachmentsWithEditor(target); }} onKeyDown={(e) => { diff --git a/lib/security/rate-limiter.ts b/lib/security/rate-limiter.ts index 5cde9cb5b..ad9c275d4 100644 --- a/lib/security/rate-limiter.ts +++ b/lib/security/rate-limiter.ts @@ -76,6 +76,9 @@ class RateLimiter { export const rateLimiters = { // 通用API限制:每分钟100次请求 api: new RateLimiter(60000, 100), + + // 聊天接口限制:每分钟60次请求(避免被高频轮询接口挤占额度) + chat: new RateLimiter(60000, 60), // 严格限制:每分钟10次请求(用于敏感操作) strict: new RateLimiter(60000, 10), @@ -117,4 +120,3 @@ export function checkRateLimit( const identifier = getClientIdentifier(request); return limiter.check(identifier); } - diff --git a/proxy.ts b/proxy.ts index 54ebfbc15..551f88287 100644 --- a/proxy.ts +++ b/proxy.ts @@ -28,7 +28,19 @@ export async function proxy(request: NextRequest) { // 🛡️ 安全检查3:速率限制 // 对API路径应用速率限制 - if (pathname.startsWith("/api/")) { + if (pathname === "/api/chat") { + const chatRateLimit = checkRateLimit(request, rateLimiters.chat); + if (!chatRateLimit.allowed) { + console.warn(`[Security] Chat rate limit exceeded for IP: ${clientIP}`, { + pathname, + resetTime: new Date(chatRateLimit.resetTime).toISOString(), + }); + return NextResponse.json( + { error: "Too many chat requests", retryAfter: Math.ceil((chatRateLimit.resetTime - Date.now()) / 1000) }, + { status: 429 } + ); + } + } else if (pathname.startsWith("/api/")) { const rateLimit = checkRateLimit(request, rateLimiters.api); if (!rateLimit.allowed) { console.warn(`[Security] Rate limit exceeded for IP: ${clientIP}`, {