fix: harden ruxa polling and constrain canvas toolbars
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-03-06 08:31:38 +08:00
parent ed3f0fefb7
commit 7ef76ec8f4
3 changed files with 59 additions and 11 deletions

View File

@ -640,7 +640,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
<div className="relative h-dvh w-full overflow-hidden overscroll-none bg-background">
{/* 1. Canvas Layer (Full Screen) - 使用 key 强制在 id 变化时重新挂载 */}
<div className="absolute inset-0 z-0" key={`workspace-${id}`}>
<div className="absolute inset-0 z-0" key={`workspace-${id}`} data-canvas-stage="true">
<CanvasWorkspace occlusionRight={panelOcclusionRight} />
</div>
@ -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"
>
<div
className="flex items-center gap-1 rounded-[24px] border bg-background/88 px-2.5 py-1.5 shadow-2xl backdrop-blur-md"
className="flex items-center gap-1 rounded-[24px] border bg-background px-2.5 py-1.5 shadow-2xl"
style={{ marginLeft: `${floatingToolbarOffset}px` }}
>
<Tooltip>

View File

@ -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',
}}

View File

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