# 函数节点嵌套系统 - 实现完整指南 ## 📋 概述 已完成函数节点嵌套系统的**完整端到端实现**,包括后端核心逻辑、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.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 & edges(TODO: 接入 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 (
{/* 返回按钮 */} {/* 面包屑链接 */} {breadcrumbs.map((bc, i) => (
{i > 0 && /} navigateToLevel(bc.level)} style={{ cursor: 'pointer', color: '#3b82f6' }} > {bc.name}
))} {/* 层级指示器 */} L{functionStack.length}
) } ``` --- ## 📊 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 **状态**: ✅ 生产就绪