diff --git a/sundynix-desktop/frontend/src/studio/StudioView.tsx b/sundynix-desktop/frontend/src/studio/StudioView.tsx index 7ed791b..f7d5131 100644 --- a/sundynix-desktop/frontend/src/studio/StudioView.tsx +++ b/sundynix-desktop/frontend/src/studio/StudioView.tsx @@ -106,12 +106,30 @@ export function StudioView({ onRun, phase, identity }: { onRun: (dsl: TaskDsl) = ); const loadGraph = useCallback( - (g: { nodes: Node[]; edges: Edge[] }) => { - setNodes((g.nodes ?? []).map((n) => ({ ...n, data: { ...n.data, status: "idle" as NodeStatus } }))); - setEdges(g.edges ?? []); + (g: { nodes?: Node[]; edges?: Edge[] }) => { + // 防御:旧/损坏数据可能缺 position/type/data —— 全部兜底,避免 React Flow 崩成黑屏。 + const safeNodes: Node[] = (g.nodes ?? []) + .filter((n) => n && n.id) + .map((n, i) => { + const d = (n.data ?? {}) as { kind?: string; label?: string; config?: Record }; + const kind = d.kind && NODE_KINDS[d.kind] ? d.kind : "output"; + const pos = n.position && typeof n.position.x === "number" && typeof n.position.y === "number" ? n.position : { x: 100 + (i % 4) * 220, y: 80 + Math.floor(i / 4) * 120 }; + return { + id: n.id, + type: "typed", + position: pos, + data: { kind, label: d.label || NODE_KINDS[kind].label, config: d.config ?? {}, status: "idle" as NodeStatus }, + }; + }); + const ids = new Set(safeNodes.map((n) => n.id)); + const safeEdges: Edge[] = (g.edges ?? []) + .filter((e) => e && e.source && e.target && ids.has(e.source) && ids.has(e.target)) + .map((e, i) => ({ ...e, id: e.id || `e${i}` })); + setNodes(safeNodes); + setEdges(safeEdges); setSelId(null); setIssues(null); - for (const n of g.nodes ?? []) { + for (const n of safeNodes) { const m = /^n(\d+)$/.exec(n.id); if (m) seq = Math.max(seq, Number(m[1])); }