diff --git a/app/api/canvas/detail-plan/route.ts b/app/api/canvas/detail-plan/route.ts index 1d8274257..9bd096b08 100644 --- a/app/api/canvas/detail-plan/route.ts +++ b/app/api/canvas/detail-plan/route.ts @@ -11,6 +11,7 @@ import { } from "@/lib/db/ai-model-flags"; import { getLanguageModelById } from "@/lib/ai/dynamic-provider"; import { inlineVisionImages } from "@/lib/ai/vision-inline"; +import { filterReachableRemoteUrls } from "@/lib/net/url-access"; export const runtime = "nodejs"; export const maxDuration = 180; @@ -1669,10 +1670,37 @@ export async function POST(request: Request) { } const baseCount = clampCount(body.count); - const referenceImages = sanitizeReferenceImages(body.referenceImages); + const referenceImagesRaw = sanitizeReferenceImages(body.referenceImages); const referenceImageNotes = sanitizeReferenceImageNotes(body.referenceImageNotes); - const modelImageUrl = String(body.modelImageUrl || "").trim(); - const bundleImageUrls = sanitizeBundleImages(body.bundleImageUrls); + const modelImageUrlRaw = String(body.modelImageUrl || "").trim(); + const bundleImageUrlsRaw = sanitizeBundleImages(body.bundleImageUrls); + const [referenceImageCheck, bundleImageCheck, modelImageCheck] = await Promise.all([ + filterReachableRemoteUrls(referenceImagesRaw, { timeoutMs: 3000 }), + filterReachableRemoteUrls(bundleImageUrlsRaw, { timeoutMs: 3000 }), + modelImageUrlRaw + ? filterReachableRemoteUrls([modelImageUrlRaw], { timeoutMs: 3000 }) + : Promise.resolve({ kept: [] as string[], dropped: [] as string[] }), + ]); + const referenceImages = referenceImageCheck.kept.slice(0, 12); + const bundleImageUrls = bundleImageCheck.kept.slice(0, 3); + const modelImageUrl = modelImageCheck.kept[0] || ""; + if (referenceImageCheck.dropped.length > 0) { + console.warn("[Canvas Detail Plan] Dropping unreachable referenceImages:", { + count: referenceImageCheck.dropped.length, + urls: referenceImageCheck.dropped.slice(0, 6), + }); + } + if (bundleImageCheck.dropped.length > 0) { + console.warn("[Canvas Detail Plan] Dropping unreachable bundleImageUrls:", { + count: bundleImageCheck.dropped.length, + urls: bundleImageCheck.dropped.slice(0, 3), + }); + } + if (modelImageUrlRaw && !modelImageUrl) { + console.warn("[Canvas Detail Plan] modelImageUrl unreachable, ignore this run:", modelImageUrlRaw); + } + const effectiveReferenceImageNotes = + referenceImages.length === referenceImagesRaw.length ? referenceImageNotes : []; const hasModelImage = /^https?:\/\//i.test(modelImageUrl); const hasBundleImages = bundleImageUrls.length > 0; const count = hasBundleImages ? Math.min(20, baseCount + 1) : baseCount; @@ -1696,7 +1724,7 @@ export async function POST(request: Request) { language, brief, coreFocus, - referenceImageNotes, + referenceImageNotes: effectiveReferenceImageNotes, hasModelImage, hasBundleImages, bundleImageCount: bundleImageUrls.length, @@ -1766,8 +1794,8 @@ export async function POST(request: Request) { `首屏标题参考(必须承接核心卖点):${coreFocus.firstScreenTitle}`, "首屏命名规则:标题必须是“名字+需求一句话”(如“甩甩杆,自拍不手抖”),不能只给一个名字。", "命名风格要求:大白话、易记、朗朗上口,未来可直接成为用户搜索词。", - referenceImageNotes.length > 0 - ? `参考图说明(按上传顺序):${referenceImageNotes.join(" | ")}` + effectiveReferenceImageNotes.length > 0 + ? `参考图说明(按上传顺序):${effectiveReferenceImageNotes.join(" | ")}` : "", hasModelImage ? "已提供固定模特图,人物场景必须复用该模特。" : "未提供模特图。", hasBundleImages diff --git a/components/canvas/CanvasWorkspace.tsx b/components/canvas/CanvasWorkspace.tsx index 6e090fab5..32913dfd1 100644 --- a/components/canvas/CanvasWorkspace.tsx +++ b/components/canvas/CanvasWorkspace.tsx @@ -348,6 +348,13 @@ export function CanvasWorkspace() { return containerRef.current.contains(target); }, []); + const clearBrowserTextSelection = useCallback(() => { + if (typeof window === "undefined") return; + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return; + selection.removeAllRanges(); + }, []); + // 处理画布点击(取消选择) const handleCanvasClick = useCallback((e: React.MouseEvent) => { if (hasBlockingDialogOpen()) return; @@ -373,12 +380,14 @@ export function CanvasWorkspace() { } if (tool === "select" && e.button === 0 && !isPointOnNodeElement(e.target)) { + e.preventDefault(); + clearBrowserTextSelection(); const start = toCanvasPosition(e.clientX, e.clientY); setIsMarqueeSelecting(true); setMarqueeStart(start); setMarqueeEnd(start); } - }, [hasBlockingDialogOpen, isEventFromCanvasContainer, isPointOnNodeElement, toCanvasPosition, tool, viewport]); + }, [clearBrowserTextSelection, hasBlockingDialogOpen, isEventFromCanvasContainer, isPointOnNodeElement, toCanvasPosition, tool, viewport]); // 处理画布平移 const handleMouseMove = useCallback((e: React.MouseEvent) => { @@ -389,6 +398,7 @@ export function CanvasWorkspace() { if (!isEventFromCanvasContainer(e.target)) return; dragPointerRef.current = toCanvasPosition(e.clientX, e.clientY); if (isPanning) { + e.preventDefault(); setViewport({ x: e.clientX - panStart.x, y: e.clientY - panStart.y, @@ -397,6 +407,7 @@ export function CanvasWorkspace() { } if (isMarqueeSelecting) { + e.preventDefault(); setMarqueeEnd(toCanvasPosition(e.clientX, e.clientY)); } }, [hasBlockingDialogOpen, isEventFromCanvasContainer, isMarqueeSelecting, isPanning, panStart, setViewport, toCanvasPosition]); @@ -406,6 +417,7 @@ export function CanvasWorkspace() { if (e && !isEventFromCanvasContainer(e.target)) return; setIsPanning(false); setSnapGuides(null); + clearBrowserTextSelection(); if (isMarqueeSelecting && marqueeStart && marqueeEnd) { const left = Math.min(marqueeStart.x, marqueeEnd.x); @@ -440,7 +452,7 @@ export function CanvasWorkspace() { setIsMarqueeSelecting(false); setMarqueeStart(null); setMarqueeEnd(null); - }, [clearSelection, isEventFromCanvasContainer, isMarqueeSelecting, marqueeEnd, marqueeStart, nodes, selectNodes, viewport.x, viewport.y, viewport.zoom]); + }, [clearBrowserTextSelection, clearSelection, isEventFromCanvasContainer, isMarqueeSelecting, marqueeEnd, marqueeStart, nodes, selectNodes, viewport.x, viewport.y, viewport.zoom]); // 新增图片后自动居中展示 const centerNodeInView = useCallback((nodeId: string) => { diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 94056d75b..da69a651b 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -1543,21 +1543,21 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { )} ) : data.status === 'generating' ? ( -