TraceStudio-dev/docs/web1.0/FIX_CRITICAL_ISSUES.md
2026-01-07 19:34:45 +08:00

16 KiB
Raw Blame History

🔧 关键问题修复报告

🐛 问题诊断

问题 1Inspector 修改参数无效

症状

  • 在 Inspector 面板修改节点参数
  • 节点本体的参数预览不更新
  • 切换节点后参数丢失

根本原因

// ❌ 旧代码:仅单向同步 Workspace → Store
React.useEffect(() => {
  setStoredNodes(nodes)  // Workspace 变化 → Store
}, [nodes, setStoredNodes])

// ❌ 缺少反向同步 Store → Workspace
// Inspector 调用 updateNodeData 更新 Store 后
// Workspace 的 nodes 没有更新!

数据流断裂

Inspector 修改参数
    ↓
updateNodeData() 更新 Store.nodes
    ↓
storedNodes 变化
    ↓
❌ Workspace 的 nodes 没有更新(缺少监听)
    ↓
React Flow 不重新渲染
    ↓
节点不刷新

问题 2聚合节点多输入未实现

症状

  • 虽然 main.py 添加了 node_logic: "aggregate"allow_multiple_inputs: true
  • 但所有节点仍然执行单输入顶替逻辑
  • Aggregator 节点无法连接多个数据源

根本原因

// ❌ 旧代码:所有节点都执行单输入顶替
setEdges((eds) => {
  const filtered = eds.filter((e) => {
    // 无条件删除目标 Handle 的旧连线
    if (e.target === connection.target && 
        e.targetHandle === connection.targetHandle) {
      return false
    }
    return true
  })
  return addEdge(connection, filtered)
})

// ❌ 没有检查 meta.allow_multiple_inputs

预期行为

  • 普通节点CSVLoader, FilterRows单输入顶替
  • 聚合节点Aggregator允许多条连线

解决方案

修复 1双向状态同步

修改文件web/src/components/Workspace.tsx

// ✅ 新代码:双向同步 Store ↔ Workspace

// Workspace → Store拖拽、删除等操作
React.useEffect(() => {
  setStoredNodes(nodes)
}, [nodes, setStoredNodes])

React.useEffect(() => {
  setStoredEdges(edges)
}, [edges, setStoredEdges])

// Store → WorkspaceInspector 修改参数)
React.useEffect(() => {
  // 使用回调形式更新,避免无限循环
  setNodes((currentNodes) => {
    // 如果 storedNodes 与当前 nodes 相同,不更新
    if (JSON.stringify(currentNodes) === JSON.stringify(storedNodes)) {
      return currentNodes
    }
    return storedNodes
  })
}, [storedNodes, setNodes])

React.useEffect(() => {
  setEdges((currentEdges) => {
    if (JSON.stringify(currentEdges) === JSON.stringify(storedEdges)) {
      return currentEdges
    }
    return storedEdges
  })
}, [storedEdges, setEdges])

修复后的数据流

Inspector 修改参数
    ↓
updateNodeData() 更新 Store.nodes创建新引用
    ↓
storedNodes 变化
    ↓
✅ useEffect 监听到 storedNodes 变化
    ↓
setNodes(storedNodes) 更新 Workspace.nodes
    ↓
React Flow 检测到 nodes 引用变化
    ↓
UniversalNode 重新渲染useEffect([data]) 触发)
    ↓
✅ 节点参数预览实时更新

优化细节

  • 使用 JSON.stringify 比较避免不必要的更新
  • 使用回调形式 setNodes((current) => ...) 避免无限循环
  • React Flow 的 useNodesState 会正确处理引用更新

修复 2聚合节点多输入逻辑

修改文件

  1. server/main.py - 添加缺失的 allow_multiple_inputs 字段
  2. web/src/components/Workspace.tsx - 实现条件分支逻辑

main.py 修复

"Aggregator": {
    "display_name": "数据聚合",
    "category": "Transform",
    "node_logic": "aggregate",        # ✅ 聚合节点标识
    "allow_multiple_inputs": True,    # ✅ 允许多输入
    "supports_preview": True,
    "inputs": [{"name": "input", "type": "DataTable"}],
    "outputs": [{"name": "result", "type": "DataTable"}],
    # ...
}

Workspace.tsx 修复

// ✅ 新代码:根据节点类型决定连线策略
const onConnect = useCallback((connection: Connection) => {
  if (!connection.source || !connection.target) return
  
  const sourceType = getNodeOutputType(nodes, connection.source, connection.sourceHandle)
  const edgeColor = TYPE_COLORS[sourceType] || TYPE_COLORS.Any
  
  // 检查目标节点是否允许多输入
  const targetNode = nodes.find(n => n.id === connection.target)
  const allowMultipleInputs = targetNode?.data?.meta?.allow_multiple_inputs === true
  
  setEdges((eds) => {
    let finalEdges = eds
    
    // 普通节点:执行单输入顶替逻辑
    if (!allowMultipleInputs) {
      finalEdges = eds.filter((e) => {
        // 删除目标 Handle 的旧连线(单输入约束)
        if (e.target === connection.target && 
            e.targetHandle === connection.targetHandle) {
          return false
        }
        return true
      })
    }
    // 聚合节点:允许多条连线到同一输入
    
    return addEdge({
      ...connection,
      type: 'default',
      animated: false,
      style: {
        stroke: edgeColor,
        strokeWidth: 3,
        opacity: 0.8,
      },
      data: {
        sourceType,
        color: edgeColor,
      }
    }, finalEdges)
  })
}, [nodes, setEdges])

逻辑流程图

用户拖拽连线 → onConnect 触发
    ↓
获取目标节点 targetNode
    ↓
检查 targetNode.data.meta.allow_multiple_inputs
    ↓
    ├─ false普通节点
    │     ↓
    │  过滤掉旧连线(单输入顶替)
    │     ↓
    │  添加新连线
    │
    └─ true聚合节点
          ↓
       保留所有旧连线
          ↓
       添加新连线(允许多输入)

🧪 测试验证

测试 1Inspector 参数修改(修复问题 1

操作步骤

1. 从 NodePalette 拖拽 "CSV 数据加载器" 到画布
   
   ┌─ CSV 数据加载器 ──────┐
   │ file_path:            │
   │ delimiter: ,          │
   └───────────────────────┘

2. 点击节点,打开 Inspector 面板

3. 修改 file_path 为 "data.csv"
   
   ✅ 预期结果:节点本体立即更新
   
   ┌─ CSV 数据加载器 [待更新] ─┐
   │ file_path: data.csv       │  ← ✅ 实时显示
   │ delimiter: ,              │
   └───────────────────────────┘

4. 点击其他节点,再点回 CSV Loader
   
   ✅ 预期结果:参数值保留为 "data.csv"

5. 刷新浏览器F5
   
   ✅ 预期结果:所有参数从 localStorage 恢复

验证点

  • 修改参数后节点立即刷新
  • 参数预览实时更新file_path: data.csv
  • 切换节点不丢失数据
  • 刷新页面数据保留
  • 显示橙色"待更新"标签
  • 预览成功后标签消失

测试 2聚合节点多输入修复问题 2

操作步骤

场景 A普通节点单输入约束

1. 拖拽 CSV Loader A
2. 拖拽 CSV Loader B
3. 拖拽 FilterRows
4. 连接 A.output → FilterRows.input
   
   ✅ 预期:显示一条连线
   
5. 连接 B.output → FilterRows.input
   
   ✅ 预期A 的连线被删除,只保留 B 的连线
   
   CSV A  ─╳──→ FilterRows  ← 旧连线删除
   CSV B  ────→ FilterRows  ← 新连线保留


场景 B聚合节点多输入支持

1. 拖拽 CSV Loader A
2. 拖拽 CSV Loader B
3. 拖拽 CSV Loader C
4. 拖拽 Aggregator数据聚合
5. 连接 A.output → Aggregator.input
   
   ✅ 预期:显示一条连线
   
6. 连接 B.output → Aggregator.input
   
   ✅ 预期A 和 B 的连线都保留
   
   CSV A  ────→ ╲
   CSV B  ────→ ─→ Aggregator  ← 两条连线共存
   
7. 连接 C.output → Aggregator.input
   
   ✅ 预期A、B、C 的连线都保留
   
   CSV A  ────→ ╲
   CSV B  ────→ ─→ Aggregator  ← 三条连线共存
   CSV C  ────→ 

验证点

  • FilterRows普通节点只保留最新连线
  • Aggregator聚合节点保留所有连线
  • 旧连线正确删除(普通节点)
  • 多条连线正确显示(聚合节点)
  • 连线颜色继承源节点类型

📊 技术对比

功能 修复前 修复后
状态同步 单向Workspace → Store 双向Workspace ↔ Store
Inspector 修改 不生效 实时刷新
参数保留 切换丢失 自动保留
脏数据标记 不显示 橙色标签
聚合节点 单输入 多输入
普通节点 单输入 单输入
连线策略 无条件顶替 条件分支

🔍 核心机制解析

1. 双向绑定的数据流

┌─────────────────────────────────────────────────────┐
│                   Zustand Store                     │
│  nodes: NodeSchema[]                                │
│  updateNodeData(id, data) {                         │
│    const nodes = get().nodes.map(n => {             │
│      if (n.id === id) {                             │
│        return { ...n, data: {...n.data, ...data} }  │ ← 新引用
│      }                                               │
│      return n                                       │
│    })                                               │
│    set({ nodes })                                   │
│    localStorage.setItem(...)                        │
│  }                                                  │
└─────────────┬────────────────────┬──────────────────┘
              │                    │
              │ storedNodes        │ setStoredNodes(nodes)
              ↓                    ↑
┌─────────────────────────────────────────────────────┐
│                   Workspace.tsx                     │
│  const [nodes, setNodes] = useNodesState()          │
│                                                     │
│  // Store → Workspace                               │
│  useEffect(() => {                                  │
│    setNodes((current) => {                          │
│      if (JSON.stringify(current) === JSON...        │
│        return current  // 避免无限循环               │
│      return storedNodes  // 应用 Store 变化         │
│    })                                               │
│  }, [storedNodes])                                  │
│                                                     │
│  // Workspace → Store                               │
│  useEffect(() => {                                  │
│    setStoredNodes(nodes)                            │
│  }, [nodes])                                        │
└─────────────┬───────────────────────────────────────┘
              │
              │ nodes prop
              ↓
┌─────────────────────────────────────────────────────┐
│                   React Flow                        │
│  <ReactFlow nodes={nodes} />                        │
│  // 检测 nodes 引用变化 → diff 算法 → 更新 DOM      │
└─────────────┬───────────────────────────────────────┘
              │
              │ node data prop
              ↓
┌─────────────────────────────────────────────────────┐
│                 UniversalNode.tsx                   │
│  const UniversalNode = ({ data, dirty }) => {       │
│    useEffect(() => {                                │
│      // data 变化时重新渲染                          │
│    }, [data])                                       │
│                                                     │
│    const value = data[key]  // 读取最新参数值        │
│    return <div>{value}</div> // 显示参数预览         │
│  }                                                  │
└─────────────────────────────────────────────────────┘

2. 聚合节点判定逻辑

// 元数据定义Python
{
  "Aggregator": {
    "node_logic": "aggregate",
    "allow_multiple_inputs": true,  // 关键字段
    // ...
  }
}

// 前端连线判定TypeScript
const targetNode = nodes.find(n => n.id === connection.target)
const allowMultipleInputs = targetNode?.data?.meta?.allow_multiple_inputs === true

if (!allowMultipleInputs) {
  // 普通节点:删除旧连线
  finalEdges = eds.filter(e => 
    !(e.target === connection.target && e.targetHandle === connection.targetHandle)
  )
} else {
  // 聚合节点:保留所有连线
  finalEdges = eds
}

3. 无限循环预防机制

问题:双向同步可能导致无限循环

Store 更新 → storedNodes 变化 → Workspace 更新 nodes
  → nodes 变化 → 触发 Store 更新 → storedNodes 变化 → ...

解决JSON 深度比较

setNodes((currentNodes) => {
  // 如果内容相同,返回当前引用(不触发更新)
  if (JSON.stringify(currentNodes) === JSON.stringify(storedNodes)) {
    return currentNodes  // 阻止循环
  }
  return storedNodes  // 应用新数据
})

优化空间

  • 使用 lodash.isEqual 替代 JSON.stringify(性能更好)
  • 添加 useRef 记录上次更新来源(避免重复同步)

🎯 修复效果总结

问题 1Inspector 参数修改

  • 根本原因:缺少 Store → Workspace 反向同步
  • 修复方案:添加 useEffect 监听 storedNodes 变化
  • 验证结果 修改参数后节点立即刷新,数据不丢失

问题 2聚合节点多输入

  • 根本原因:未检查 allow_multiple_inputs 字段
  • 修复方案:条件分支逻辑 + 后端元数据补全
  • 验证结果 Aggregator 支持多条连线FilterRows 保持单输入

额外优化

  • 脏数据标记(橙色"待更新"标签)
  • 自动持久化到 localStorage
  • 预览成功后清除脏标记
  • 无限循环预防机制

📁 修改文件清单

  1. web/src/components/Workspace.tsx

    • 添加 Store → Workspace 双向同步
    • 实现聚合节点多输入逻辑
    • 添加 JSON 深度比较防止循环
  2. server/main.py

    • Aggregator 添加 allow_multiple_inputs: True
  3. web/src/stores/useStore.ts (已在前次完成)

    • updateNodeData 返回新引用
    • 自动标记 dirty
    • 自动持久化 localStorage
  4. web/src/components/nodes/UniversalNode.tsx (已在前次完成)

    • useEffect 监听 data 变化
    • 显示脏数据标签

🚀 启动测试

# 终端 1启动后端
cd server
python main.py

# 终端 2启动前端
cd web
npm run dev

测试清单

  1. 打开 http://localhost:5173
  2. 拖拽 CSV Loader 到画布
  3. 在 Inspector 修改 file_path
  4. 验证节点实时刷新
  5. 切换到其他节点再切回来
  6. 验证参数保留
  7. 拖拽两个 CSV Loader + 一个 Aggregator
  8. 连接两个 Loader 到 Aggregator
  9. 验证两条连线共存

修复完成时间2026-01-07
状态 两个核心问题已完全解决