diff --git a/src/components/PopiaiEmbedBridge.tsx b/src/components/PopiaiEmbedBridge.tsx index e163fa0a..9ec694d9 100644 --- a/src/components/PopiaiEmbedBridge.tsx +++ b/src/components/PopiaiEmbedBridge.tsx @@ -4,7 +4,8 @@ import { useEffect, useMemo } from "react"; import { useWorkflowStore } from "@/store/workflowStore"; import type { WorkflowFile } from "@/store/workflowStore"; import type { EditOperation } from "@/lib/chat/editOperations"; -import type { WorkflowEdge, WorkflowNode } from "@/types"; +import { getDefaultNodeDimensions } from "@/constants/nodeDimensions"; +import type { NodeType, WorkflowEdge, WorkflowNode } from "@/types"; const SOURCE_POPIAI = "popiai"; const SOURCE_POPITV = "popitv"; @@ -28,39 +29,132 @@ interface BridgeMessage { interface NodeDimensions { id: string; + x: number; + y: number; width: number; height: number; } -function measureNodes(nodeIds: string[]): NodeDimensions[] { - const state = useWorkflowStore.getState(); +function measureNodesFromDom(nodeIds: string[]): NodeDimensions[] { const measurements: NodeDimensions[] = []; for (const nodeId of nodeIds) { - const node = state.nodes.find(n => n.id === nodeId); - if (node?.measured) { - measurements.push({ - id: nodeId, - width: node.measured.width ?? 0, - height: node.measured.height ?? 0, - }); - } else { - // 如果 measured 不存在,尝试从 DOM 获取 + // 使用 try-catch 包装,避免 DOM 操作可能抛出的异常 + try { + // 直接从 DOM 获取节点尺寸,避免访问 store 可能触发的状态更新 const nodeElement = document.querySelector(`[data-id="${nodeId}"]`); if (nodeElement) { const rect = nodeElement.getBoundingClientRect(); measurements.push({ id: nodeId, + x: rect.left, + y: rect.top, width: rect.width, height: rect.height, }); + } else { + // 如果 DOM 元素不存在,使用默认尺寸 + measurements.push({ + id: nodeId, + x: 0, + y: 0, + width: 0, + height: 0, + }); } + } catch { + measurements.push({ + id: nodeId, + x: 0, + y: 0, + width: 0, + height: 0, + }); } } return measurements; } +const MEASUREMENT_CACHE_TTL_MS = 500; +const nodeMeasurementCache = new Map(); + +function readPositiveNumber(value: unknown): number | null { + if (typeof value === "number" && Number.isFinite(value) && value > 0) return value; + if (typeof value === "string") { + const parsed = Number.parseFloat(value); + if (Number.isFinite(parsed) && parsed > 0) return parsed; + } + return null; +} + +function dimensionsFromStoreNode(node: WorkflowNode): NodeDimensions { + const nodeWithRuntimeSize = node as WorkflowNode & { + width?: unknown; + height?: unknown; + style?: { width?: unknown; height?: unknown }; + }; + const defaults = getDefaultNodeDimensions(node.type as NodeType); + + return { + id: node.id, + x: node.position.x, + y: node.position.y, + width: + readPositiveNumber(node.measured?.width) ?? + readPositiveNumber(nodeWithRuntimeSize.width) ?? + readPositiveNumber(nodeWithRuntimeSize.style?.width) ?? + defaults.width, + height: + readPositiveNumber(node.measured?.height) ?? + readPositiveNumber(nodeWithRuntimeSize.height) ?? + readPositiveNumber(nodeWithRuntimeSize.style?.height) ?? + defaults.height, + }; +} + +function measureNodes(nodeIds: string[]): NodeDimensions[] { + const now = Date.now(); + const stateNodesById = new Map(useWorkflowStore.getState().nodes.map((node) => [node.id, node])); + const measurementsById = new Map(); + const idsMissingFromStore: string[] = []; + + for (const nodeId of nodeIds) { + const node = stateNodesById.get(nodeId); + if (node) { + const dimensions = dimensionsFromStoreNode(node); + measurementsById.set(nodeId, dimensions); + nodeMeasurementCache.set(nodeId, { ...dimensions, timestamp: now }); + continue; + } + + const cached = nodeMeasurementCache.get(nodeId); + if (cached && now - cached.timestamp < MEASUREMENT_CACHE_TTL_MS) { + measurementsById.set(nodeId, { + id: cached.id, + x: cached.x, + y: cached.y, + width: cached.width, + height: cached.height, + }); + continue; + } + + idsMissingFromStore.push(nodeId); + } + + if (idsMissingFromStore.length > 0) { + for (const dimensions of measureNodesFromDom(idsMissingFromStore)) { + measurementsById.set(dimensions.id, dimensions); + if (dimensions.width > 0 && dimensions.height > 0) { + nodeMeasurementCache.set(dimensions.id, { ...dimensions, timestamp: now }); + } + } + } + + return nodeIds.map((nodeId) => measurementsById.get(nodeId) ?? { id: nodeId, x: 0, y: 0, width: 0, height: 0 }); +} + function isSafeParentOrigin(origin: string | null): origin is string { if (!origin) return false; return origin === "file://" || /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(origin);