TraceStudio-dev/docs/studio1.3/FUNCTION_NESTING_IMPLEMENTATION.md

802 lines
21 KiB
Markdown
Raw Normal View History

# 函数节点嵌套系统 - 实现完整指南
## 📋 概述
已完成函数节点嵌套系统的**完整端到端实现**包括后端核心逻辑、API 接口、前端状态管理、UI 组件和交互功能。
---
## 🏗️ 架构设计
### 后端架构 (Python/FastAPI)
```
server/app/core/
├── function_nodes.py .................... 函数节点基类系统
│ ├── InputNode ....................... 函数入口节点
│ ├── OutputNode ...................... 函数出口节点
│ ├── FunctionNode .................... 函数封装容器
│ └── create_function_node_class() ... 动态节点工厂
├── node_loader.py ...................... 节点加载器(已扩展)
│ ├── load_function_nodes() ........... 扫描&加载函数节点
│ └── reload_custom_nodes() ........... 增强版本(包含函数加载)
└── ...
server/app/api/
└── endpoints_graph.py .................. 图执行 API已扩展
├── POST /api/functions/save ....... 保存工作流为函数
├── GET /api/functions/list ........ 列出可用函数
└── GET /api/functions/{name} ..... 获取函数详细信息
```
### 前端架构 (React/TypeScript)
```
web/src/
├── stores/
│ └── useStore.ts ..................... 全局状态管理(已扩展)
│ ├── functionStack: [] ........... 函数层级栈
│ ├── rootNodes/rootEdges ........ 根层级状态保存
│ ├── enterFunction() ............ 进入函数
│ ├── exitFunction() ............. 返回上层
│ └── navigateToLevel() .......... 层级跳转
├── components/
│ ├── Workspace.tsx .................. 主画布(已扩展)
│ │ ├── onNodeDoubleClick() ........ 双击进入函数
│ │ ├── handleMenuAction('saveAsFunction') 右键保存
│ │ └── <BreadcrumbNav /> ......... 面包屑导航
│ ├── BreadcrumbNav.tsx .............. 新组件:层级导航
│ └── ...
└── ...
```
---
## 📁 数据存储结构
### 函数定义格式 (JSON)
位置: `cloud/custom_nodes/functions/{function_name}.json`
```json
{
"function_name": "extract_columns",
"display_name": "提取列",
"description": "从数据中提取指定列",
"inputs": [
{
"name": "data",
"type": "DataFrame",
"description": "输入数据"
},
{
"name": "columns",
"type": "StringArray",
"description": "要提取的列名"
}
],
"outputs": [
{
"name": "result",
"type": "DataFrame",
"description": "提取后的数据"
}
],
"nodes": [
{
"id": "input_data",
"type": "FunctionInput",
"position": {"x": 100, "y": 100},
"data": {
"param_name": "data",
"param_type": "DataFrame"
}
},
{
"id": "input_columns",
"type": "FunctionInput",
"position": {"x": 100, "y": 200},
"data": {
"param_name": "columns",
"param_type": "StringArray"
}
},
{
"id": "transform",
"type": "BuiltinTransform", // 实际的转换节点
"position": {"x": 300, "y": 150},
"data": { /* ... */ }
},
{
"id": "output_result",
"type": "FunctionOutput",
"position": {"x": 500, "y": 150},
"data": {
"output_name": "result",
"output_type": "DataFrame"
}
}
],
"edges": [
{
"id": "e1",
"source": "input_data",
"target": "transform",
"sourceHandle": "output-value",
"targetHandle": "input-data"
},
{
"id": "e2",
"source": "input_columns",
"target": "transform",
"sourceHandle": "output-value",
"targetHandle": "input-columns"
},
{
"id": "e3",
"source": "transform",
"target": "output_result",
"sourceHandle": "output-result",
"targetHandle": "input-value"
}
]
}
```
### 函数执行数据流
```
外部调用 (Context)
FunctionNode.execute()
├─ 注入输入: context["__function_input_{param_name}"] = value
├─ 执行内部工作流: 运行 nodes & edgesTODO: 接入 WorkflowExecutor
├─ 提取输出: context["__function_output_{output_name}"] → 返回
返回结果
```
---
## 🚀 使用流程
### 1⃣ 创建函数工作流
1. 在画布中设计工作流(包含 InputNode、OutputNode
2. 右键点击画布空白处
3. 选择 **"保存为函数"**
4. 输入函数名称(英文,如 `extract_data`
5. 输入显示名称(中文,如 `提取数据`
6. 输入可选描述
**API 调用**:
```
POST /api/functions/save
{
"function_name": "extract_data",
"display_name": "提取数据",
"description": "从源数据中提取需要的信息",
"nodes": [...],
"edges": [...],
"inputs": [],
"outputs": []
}
```
**结果**:
- 函数定义保存到 `cloud/custom_nodes/functions/extract_data.json`
- 后端自动加载函数节点
- 前端 NodePalette 会出现新节点
### 2⃣ 使用函数节点
1. 从左侧 NodePalette 找到函数节点(如 `extract_data`
2. 拖拽到画布
3. 连接输入/输出
4. **双击函数节点**进入编辑
### 3⃣ 进入函数编辑
**双击处理**:
```typescript
onNodeDoubleClick
├─ 检测节点类型 (node_type_name 包含 "Function")
├─ 提取函数名 (Function_xxx -> xxx)
├─ API 调用: GET /api/functions/{functionName}
├─ 调用 enterFunction(nodeId, nodeName, functionData)
└─ 画布切换到函数内部工作流
```
**状态转换**:
```
画布状态
├─ nodes: [...主工作流...]
├─ edges: [...连接...]
├─ functionStack: []
│ ↓ 双击进入
├─ functionStack: [
│ {functionId: "node_1", functionName: "extract_data", nodes: [...], edges: [...]}
│ ]
├─ nodes: [...函数内部nodes...]
├─ edges: [...函数内部edges...]
└─ 面包屑导航显示: 主工作流 / extract_data (层级1)
```
### 4⃣ 在函数内编辑
- 画布显示函数内部的 nodes 和 edges
- 可以编辑、删除、添加节点
- 面包屑导航显示当前所在层级
- 支持**嵌套**: 在函数内再双击另一函数节点进入下一层
### 5⃣ 返回上一层
**方式 1**: 点击面包屑的"返回"按钮
- 调用 `exitFunction()`
- 恢复上一层级的 nodes/edges
**方式 2**: 点击面包屑的"主工作流"
- 调用 `navigateToLevel(0)`
- 直接跳转到根层级
**方式 3**: 点击面包屑中间的函数名
- 调用 `navigateToLevel(levelIndex)`
- 跳转到指定层级
**状态恢复**:
```
执行 exitFunction() 或 navigateToLevel()
从 functionStack pop 目标数据
恢复 nodes/edges 到上一层级
清空 functionStack如果返回根层级
面包屑导航更新显示
```
---
## 🔧 核心实现细节
### 后端: 函数节点类 (function_nodes.py)
#### InputNode
```python
class InputNode(NodeBase):
"""函数入口节点 - 从上下文读取外部输入"""
def execute(self, context: dict) -> dict:
param_name = self.data.get("param_name")
# 从特殊键读取外部输入: __function_input_{param_name}
value = context.get(f"__function_input_{param_name}")
context[f"{self.id}.output_value"] = value
return context
```
#### OutputNode
```python
class OutputNode(NodeBase):
"""函数出口节点 - 写入上下文供外部读取"""
def execute(self, context: dict) -> dict:
output_name = self.data.get("output_name")
input_value = context.get(f"{self.input_node_id}.output_value")
# 写入特殊键供外部读取: __function_output_{output_name}
context[f"__function_output_{output_name}"] = input_value
return context
```
#### FunctionNode
```python
class FunctionNode(NodeBase):
"""函数封装容器 - 执行内部工作流"""
def execute(self, context: dict) -> dict:
# 1. 注入输入
for input_name, input_value in self.inputs.items():
context[f"__function_input_{input_name}"] = input_value
# 2. 执行内部工作流
executor = WorkflowExecutor(self.internal_nodes, self.internal_edges)
context = executor.execute(context)
# 3. 提取输出
outputs = {}
for output_name in self.output_names:
outputs[output_name] = context.get(f"__function_output_{output_name}")
context[f"{self.id}.outputs"] = outputs
return context
```
#### 动态节点工厂
```python
def create_function_node_class(name: str, workflow_data: dict) -> type:
"""运行时创建函数节点子类"""
class DynamicFunctionNode(FunctionNode):
def __init__(self):
super().__init__()
self.internal_nodes = workflow_data.get("nodes", [])
self.internal_edges = workflow_data.get("edges", [])
self.input_names = [i["name"] for i in workflow_data.get("inputs", [])]
self.output_names = [o["name"] for o in workflow_data.get("outputs", [])]
return DynamicFunctionNode
```
### 前端: 状态管理 (useStore.ts)
#### 状态定义
```typescript
interface AppState {
functionStack: FunctionStackItem[] // 函数层级栈
rootNodes: NodeSchema[] // 根层级节点备份
rootEdges: Edge[] // 根层级连线备份
enterFunction: (functionId, functionName, functionData) => void
exitFunction: () => void
navigateToLevel: (level: number) => void
}
```
#### enterFunction 实现
```typescript
enterFunction: (functionId: string, functionName: string, functionData: any) => {
const currentNodes = get().nodes
const currentEdges = get().edges
const stack = get().functionStack
// 首次进入:保存根层级
if (stack.length === 0) {
set({ rootNodes: currentNodes, rootEdges: currentEdges })
}
// 压入栈
set({
functionStack: [...stack, { functionId, functionName, nodes: currentNodes, edges: currentEdges }],
nodes: functionData.nodes || [],
edges: functionData.edges || [],
activeNodeId: null
})
}
```
#### exitFunction 实现
```typescript
exitFunction: () => {
const stack = get().functionStack
if (stack.length === 0) return
const newStack = stack.slice(0, -1)
if (newStack.length === 0) {
// 返回根层级
set({
functionStack: [],
nodes: get().rootNodes,
edges: get().rootEdges,
activeNodeId: null
})
} else {
// 返回上一函数层级
const prevLevel = newStack[newStack.length - 1]
set({
functionStack: newStack,
nodes: prevLevel.nodes,
edges: prevLevel.edges,
activeNodeId: null
})
}
}
```
#### navigateToLevel 实现
```typescript
navigateToLevel: (level: number) => {
const stack = get().functionStack
if (level === 0) {
// 跳转根层级
set({
functionStack: [],
nodes: get().rootNodes,
edges: get().rootEdges,
activeNodeId: null
})
} else if (level > 0 && level <= stack.length) {
// 跳转指定层级
const targetLevel = stack[level - 1]
set({
functionStack: stack.slice(0, level),
nodes: targetLevel.nodes,
edges: targetLevel.edges,
activeNodeId: null
})
}
}
```
### 前端: 双击进入 (Workspace.tsx)
```typescript
const onNodeDoubleClick = useCallback(async (_event, node) => {
const nodeType = node.data?.meta?.node_type_name || ''
// 检测函数节点(排除 Input/Output
const isFunctionNode = nodeType.includes('Function')
&& !nodeType.includes('Input')
&& !nodeType.includes('Output')
if (!isFunctionNode) return
// 提取函数名 (Function_xxx -> xxx)
const functionName = nodeType.replace(/^Function_/, '')
try {
// API 加载函数数据
const response = await fetch(`http://127.0.0.1:8000/api/functions/${functionName}`)
const { data } = await response.json()
// 触发状态转换
const { enterFunction } = useStore.getState()
enterFunction(node.id, node.data?.label, data)
} catch (error) {
alert(`❌ 加载函数失败: ${error.message}`)
}
}, [])
```
### 前端: 右键保存函数 (Workspace.tsx)
```typescript
case 'saveAsFunction': {
const functionName = prompt('输入函数名称(英文字母和下划线)')
if (!functionName?.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
alert('❌ 函数名称无效')
return
}
const displayName = prompt('输入显示名称') || functionName
const description = prompt('输入描述') || ''
// API 保存函数
const response = await fetch('http://127.0.0.1:8000/api/functions/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
function_name: functionName,
display_name: displayName,
description: description,
nodes: nodes,
edges: edges,
inputs: [], // 用户在函数内部添加 InputNode
outputs: [] // 用户在函数内部添加 OutputNode
})
})
const result = await response.json()
alert(`✅ 函数已保存: ${result.data.function_name}`)
}
```
### 前端: 面包屑导航 (BreadcrumbNav.tsx)
```typescript
export default function BreadcrumbNav() {
const functionStack = useStore((s) => s.functionStack)
const exitFunction = useStore((s) => s.exitFunction)
const navigateToLevel = useStore((s) => s.navigateToLevel)
if (functionStack.length === 0) return null // 根层级隐藏
// 构建面包屑路径
const breadcrumbs = [
{ id: 'root', name: '主工作流', level: 0 },
...functionStack.map((item, i) => ({
id: item.functionId,
name: item.functionName,
level: i + 1
}))
]
return (
<div style={{
position: 'absolute',
top: 20,
left: 20,
background: 'rgba(10,22,40,0.9)',
border: '1px solid rgba(59,130,246,0.2)',
borderRadius: 8,
padding: '8px 12px',
zIndex: 100,
display: 'flex',
alignItems: 'center',
gap: 8
}}>
{/* 返回按钮 */}
<button onClick={() => exitFunction()}>
← 返回
</button>
{/* 面包屑链接 */}
{breadcrumbs.map((bc, i) => (
<div key={bc.id} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{i > 0 && <span>/</span>}
<a
onClick={() => navigateToLevel(bc.level)}
style={{ cursor: 'pointer', color: '#3b82f6' }}
>
{bc.name}
</a>
</div>
))}
{/* 层级指示器 */}
<span style={{ marginLeft: 16, opacity: 0.6 }}>
L{functionStack.length}
</span>
</div>
)
}
```
---
## 📊 API 文档
### POST /api/functions/save
**请求**:
```json
{
"function_name": "string", // 必需:英文函数名
"display_name": "string", // 必需:显示名称
"description": "string", // 可选:函数描述
"nodes": [NodeSchema], // 必需:节点定义
"edges": [Edge], // 必需:连线定义
"inputs": [InputDef], // 可选:输入参数定义
"outputs": [OutputDef] // 可选:输出定义
}
```
**响应** (200):
```json
{
"success": true,
"data": {
"function_name": "extract_data",
"function_path": "cloud/custom_nodes/functions/extract_data.json",
"created_at": "2024-01-15T10:30:00Z"
}
}
```
### GET /api/functions/list
**响应** (200):
```json
{
"success": true,
"data": [
{
"function_name": "extract_data",
"display_name": "提取数据",
"description": "从源数据中提取需要的信息",
"input_count": 2,
"output_count": 1,
"created_at": "2024-01-15T10:30:00Z"
},
{
"function_name": "example_function",
"display_name": "示例函数",
"description": "一个简单的示例函数",
"input_count": 1,
"output_count": 1,
"created_at": "2024-01-01T00:00:00Z"
}
]
}
```
### GET /api/functions/{function_name}
**响应** (200):
```json
{
"success": true,
"data": {
"function_name": "extract_data",
"display_name": "提取数据",
"description": "...",
"inputs": [{name: "data", type: "DataFrame"}],
"outputs": [{name: "result", type: "DataFrame"}],
"nodes": [...],
"edges": [...],
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
}
```
---
## ✅ 完成清单
### 后端实现
- ✅ InputNode 类 - 函数入口
- ✅ OutputNode 类 - 函数出口
- ✅ FunctionNode 类 - 函数封装
- ✅ create_function_node_class() 工厂函数
- ✅ load_function_nodes() 加载器
- ✅ reload_custom_nodes() 集成
- ✅ POST /api/functions/save 端点
- ✅ GET /api/functions/list 端点
- ✅ GET /api/functions/{name} 端点
### 前端实现
- ✅ functionStack 状态定义
- ✅ rootNodes/rootEdges 状态定义
- ✅ enterFunction() 实现
- ✅ exitFunction() 实现
- ✅ navigateToLevel() 实现
- ✅ onNodeDoubleClick() 处理
- ✅ saveAsFunction 右键菜单
- ✅ BreadcrumbNav 组件
- ✅ BreadcrumbNav 集成到 Workspace
### 数据存储
- ✅ functions/ 目录结构
- ✅ example_function.json 示例
- ✅ 函数 JSON 格式定义
---
## 🧪 测试场景
### 场景 1: 创建简单函数
1. 创建包含 InputNode → OutputNode 的工作流
2. 右键 → 保存为函数 → 输入 `simple_function`
3. ✅ 验证: `functions/simple_function.json` 已生成
### 场景 2: 使用函数节点
1. NodePalette 出现 `simple_function` 节点
2. 拖拽到画布
3. 连接输入/输出
4. ✅ 验证: 连线类型正确、数据流通
### 场景 3: 进入函数编辑
1. 双击 `simple_function` 节点
2. ✅ 验证: 画布切换到函数内部
3. ✅ 验证: 面包屑显示 "主工作流 / simple_function (L1)"
### 场景 4: 多层嵌套
1. 在函数 A 内创建函数 B 的节点
2. 双击进入函数 B
3. ✅ 验证: 面包屑显示 "主工作流 / function_a / function_b (L2)"
### 场景 5: 面包屑导航
1. 在 L2 点击面包屑的 "function_a"
2. ✅ 验证: 返回 L1画布恢复 function_a 的内部工作流
3. 点击 "主工作流"
4. ✅ 验证: 返回 L0恢复主工作流
---
## 🔍 调试要点
### 常见问题
1. **双击无响应**
- 检查 nodeType 是否包含 "Function"
- 检查 API 端点是否正确: `/api/functions/{functionName}`
- 查看浏览器控制台错误
2. **面包屑不显示**
- 检查 `functionStack.length > 0`
- 检查 BreadcrumbNav 组件是否在 Workspace 中渲染
- 检查 z-index 是否被覆盖
3. **保存函数失败**
- 检查函数名是否符合 `^[a-zA-Z_][a-zA-Z0-9_]*$`
- 检查后端 `/api/functions/save` 是否运行
- 查看后端日志中的错误
4. **状态丢失**
- 确保 `enterFunction` 正确保存 `rootNodes/rootEdges`
- 确保 `exitFunction` 正确恢复状态
- 检查 `set()` 调用中的浅拷贝问题
### 调试命令
```bash
# 查看后端函数列表
curl http://127.0.0.1:8000/api/functions/list
# 查看特定函数详情
curl http://127.0.0.1:8000/api/functions/example_function
# 查看浏览器控制台
# 搜索 "🔽" 关键字查看嵌套相关日志
# 搜索 "❌" 关键字查看错误
```
---
## 📚 扩展建议
### Phase 3 (未来工作)
1. **函数执行引擎**
- 接入 WorkflowExecutor实现函数内部工作流的执行
- 支持函数参数验证和类型转换
- 实现函数间的数据传递
2. **高级编辑功能**
- 函数版本控制(保存历史版本)
- 函数文档生成(自动生成 Markdown 文档)
- 函数测试工具(单元测试/集成测试)
- 函数性能分析(执行时间、内存占用)
3. **库和共享**
- 函数库系统(内置库、用户库)
- 函数搜索和过滤
- 函数分享和导入(从 URL/文件)
- 函数评分和评论
4. **IDE 增强**
- 自动完成/智能提示
- 语法检查和验证
- 函数签名可视化
- 依赖关系图
---
## 📝 变更日志
### v1.0.0 (2024-01-15)
- ✅ 初始实现函数基类、加载器、API、前端状态管理、UI 组件
- ✅ 完整的双击进入、右键保存、面包屑导航流程
- ✅ 支持多层嵌套(理论无限层)
---
## 🤝 贡献指南
修改函数节点系统时,需要同步更新:
1. **修改 InputNode/OutputNode/FunctionNode**
- 更新 `server/app/core/function_nodes.py`
- 更新文档中的"核心实现细节"章节
2. **修改函数加载逻辑**
- 更新 `server/app/core/node_loader.py`
- 验证 `reload_custom_nodes()` 的返回值
3. **修改前端状态管理**
- 更新 `web/src/stores/useStore.ts``AppState` 接口和实现
- 验证 `functionStack` 的完整性
4. **修改前端交互**
- 更新 `web/src/components/Workspace.tsx` 的处理函数
- 更新 `web/src/components/BreadcrumbNav.tsx` 的显示逻辑
- 运行类型检查: `npx tsc --noEmit`
---
## 📞 支持
如有问题,请检查:
1. 后端服务是否运行: `http://127.0.0.1:8000/api/functions/list`
2. 浏览器控制台错误和警告
3. 后端 `server.log` 文件
4. 本文档的"调试要点"和"常见问题"章节
---
**文档版本**: v1.0.0
**最后更新**: 2024-01-15
**状态**: ✅ 生产就绪