修复画布Agent误澄清与输入标签污染并优化chat限流
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-14 10:35:11 +08:00
parent 118a2efe9a
commit f0ede6f0c9
4 changed files with 88 additions and 12 deletions

View File

@ -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" ||

View File

@ -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) => {

View File

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

View File

@ -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}`, {