# 🔧 关键问题修复报告
## 🐛 问题诊断
### 问题 1:Inspector 修改参数无效 ❌
**症状**:
- 在 Inspector 面板修改节点参数
- 节点本体的参数预览不更新
- 切换节点后参数丢失
**根本原因**:
```typescript
// ❌ 旧代码:仅单向同步 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 节点无法连接多个数据源
**根本原因**:
```typescript
// ❌ 旧代码:所有节点都执行单输入顶替
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`
```typescript
// ✅ 新代码:双向同步 Store ↔ Workspace
// Workspace → Store(拖拽、删除等操作)
React.useEffect(() => {
setStoredNodes(nodes)
}, [nodes, setStoredNodes])
React.useEffect(() => {
setStoredEdges(edges)
}, [edges, setStoredEdges])
// Store → Workspace(Inspector 修改参数)
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 修复
```python
"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 修复
```typescript
// ✅ 新代码:根据节点类型决定连线策略
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(聚合节点)
↓
保留所有旧连线
↓
添加新连线(允许多输入)
```
---
## 🧪 测试验证
### 测试 1:Inspector 参数修改(修复问题 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 恢复
```
**验证点**:
- [x] 修改参数后节点立即刷新
- [x] 参数预览实时更新(file_path: data.csv)
- [x] 切换节点不丢失数据
- [x] 刷新页面数据保留
- [x] 显示橙色"待更新"标签
- [x] 预览成功后标签消失
---
### 测试 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 ────→ ╱
```
**验证点**:
- [x] FilterRows(普通节点):只保留最新连线
- [x] Aggregator(聚合节点):保留所有连线
- [x] 旧连线正确删除(普通节点)
- [x] 多条连线正确显示(聚合节点)
- [x] 连线颜色继承源节点类型
---
## 📊 技术对比
| 功能 | 修复前 | 修复后 |
|------|--------|--------|
| **状态同步** | 单向(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 │
│