TraceStudio-dev/docs/studio1.3/FUNCTION_NESTING_IMPLEMENTATION.md
2026-01-09 21:37:02 +08:00

802 lines
21 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 函数节点嵌套系统 - 实现完整指南
## 📋 概述
已完成函数节点嵌套系统的**完整端到端实现**包括后端核心逻辑、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
**状态**: ✅ 生产就绪