|
|
|
@ -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<string, NodeDimensions & { timestamp: number }>(); |
|
|
|
|
|
|
|
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<string, NodeDimensions>(); |
|
|
|
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); |
|
|
|
|