From 7ef76ec8f4d244122912dcf8ad8d35db6dc2d2de Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 6 Mar 2026 08:31:38 +0800 Subject: [PATCH] fix: harden ruxa polling and constrain canvas toolbars --- app/canvas/[id]/page.tsx | 5 ++-- components/canvas/nodes/ImageNode.tsx | 37 ++++++++++++++++++++++----- lib/ai/providers/ruxa-image.ts | 28 ++++++++++++++++++-- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index bff15d394..d5160b592 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -640,7 +640,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
{/* 1. Canvas Layer (Full Screen) - 使用 key 强制在 id 变化时重新挂载 */} -
+
@@ -686,6 +686,7 @@ export default function CanvasEditorPage({ params }: PageProps) { "absolute z-10 transition-all duration-300 ease-in-out", isPanelOpen ? "translate-x-0 opacity-100" : "translate-x-[120%] opacity-0 pointer-events-none" )} + data-canvas-right-panel="true" style={{ top: DOCKED_PANEL_INSET, right: DOCKED_PANEL_INSET, @@ -720,7 +721,7 @@ export default function CanvasEditorPage({ params }: PageProps) { data-canvas-floating-toolbar="true" >
diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 429d4ad1b..2f7e1886b 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -874,7 +874,6 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { let unsafeBottomTop = window.innerHeight; const bottomOverlaySelectors = [ - '[data-canvas-chat-input-anchor="true"]', '[data-canvas-floating-toolbar="true"]', ]; @@ -905,26 +904,50 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { } top = Math.min(Math.max(top, viewportPadding), maxTop); - const maxToolbarWidth = window.innerWidth - viewportPadding * 2; + const stageElement = document.querySelector('[data-canvas-stage="true"]') as HTMLElement | null; + const panelElement = document.querySelector('[data-canvas-right-panel="true"]') as HTMLElement | null; + + let safeLeft = viewportPadding; + let safeRight = window.innerWidth - viewportPadding; + if (stageElement) { + const stageRect = stageElement.getBoundingClientRect(); + if (stageRect.width > 0) { + safeLeft = Math.max(safeLeft, stageRect.left + viewportPadding); + safeRight = Math.min(safeRight, stageRect.right - viewportPadding); + } + } + if (panelElement) { + const panelRect = panelElement.getBoundingClientRect(); + if (panelRect.width > 0 && panelRect.left < window.innerWidth) { + safeRight = Math.min(safeRight, panelRect.left - viewportPadding); + } + } + if (safeRight <= safeLeft + 8) { + safeLeft = viewportPadding; + safeRight = window.innerWidth - viewportPadding; + } + + const maxToolbarWidth = Math.max(180, safeRight - safeLeft); const effectiveToolbarWidth = Math.min(toolbarWidth, maxToolbarWidth); const halfToolbar = effectiveToolbarWidth / 2; let left = rect.left + rect.width / 2; if (effectiveToolbarWidth >= maxToolbarWidth - 1) { - left = window.innerWidth / 2; + left = (safeLeft + safeRight) / 2; } else { left = Math.min( - Math.max(left, viewportPadding + halfToolbar), - window.innerWidth - viewportPadding - halfToolbar + Math.max(left, safeLeft + halfToolbar), + safeRight - halfToolbar ); } return { top, left, + maxWidth: maxToolbarWidth, }; } - return { top: 0, left: 0 }; + return { top: 0, left: 0, maxWidth: 480 }; }; const getTextPanelPosition = () => { @@ -2184,7 +2207,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { transform: 'translateX(-50%) scale(0.8)', transformOrigin: 'top center', pointerEvents: 'auto', - maxWidth: 'calc(100vw - 24px)', + maxWidth: `${Math.max(180, Math.floor(toolbarPosition.maxWidth))}px`, overflowX: 'auto', scrollbarWidth: 'none', }} diff --git a/lib/ai/providers/ruxa-image.ts b/lib/ai/providers/ruxa-image.ts index ef1ddaeed..6d3a02156 100644 --- a/lib/ai/providers/ruxa-image.ts +++ b/lib/ai/providers/ruxa-image.ts @@ -24,7 +24,7 @@ const DEFAULT_SIZE = "1:1"; const DEFAULT_RESOLUTION = "1k"; const DEFAULT_OUTPUT_FORMAT = "png"; const POLL_INTERVAL_MS = 2000; -const POLL_MAX_ATTEMPTS = 60; +const POLL_MAX_ATTEMPTS = 90; function normalizeUrlForCompare(url: string) { try { @@ -109,10 +109,24 @@ function extractStatus(payload: any): string | undefined { } function extractErrorMessage(payload: any): string { + let parsedResultJson: any = null; + if (typeof payload?.resultJson === "string" && payload.resultJson.trim()) { + try { + parsedResultJson = JSON.parse(payload.resultJson); + } catch { + parsedResultJson = null; + } + } + return ( + payload?.failMsg || + payload?.data?.failMsg || payload?.message || payload?.error || payload?.errorMessage || + parsedResultJson?.failMsg || + parsedResultJson?.error || + payload?.resultJson || payload?.data?.message || payload?.data?.error || "RUXA 任务执行失败" @@ -319,7 +333,7 @@ async function waitForTaskResult(baseURL: string, apiKey: string, taskId: string return imageUrls; } - if (status && ["failed", "error", "cancelled", "canceled"].includes(status)) { + if (status && ["failed", "fail", "error", "cancelled", "canceled"].includes(status)) { throw new Error(extractErrorMessage(payload)); } @@ -401,6 +415,16 @@ export function createRuxaImage({ apiKey, baseURL }: RuxaImageConfig) { ); } + const createCode = + typeof createPayload?.code === "number" + ? createPayload.code + : Number.isFinite(Number(createPayload?.code)) + ? Number(createPayload.code) + : null; + if (createCode !== null && createCode !== 200) { + throw new Error(extractErrorMessage(createPayload)); + } + let outputUrls = filterSourceImages(collectImageUrls(createPayload), imageUrls); if (outputUrls.length === 0) { const taskId = extractTaskId(createPayload);