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

21 KiB
Raw Blame 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

{
  "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 进入函数编辑

双击处理:

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

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

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

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

动态节点工厂

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)

状态定义

interface AppState {
  functionStack: FunctionStackItem[]     // 函数层级栈
  rootNodes: NodeSchema[]                // 根层级节点备份
  rootEdges: Edge[]                      // 根层级连线备份
  
  enterFunction: (functionId, functionName, functionData) => void
  exitFunction: () => void
  navigateToLevel: (level: number) => void
}

enterFunction 实现

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 实现

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 实现

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)

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)

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)

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

请求:

{
  "function_name": "string",        // 必需:英文函数名
  "display_name": "string",         // 必需:显示名称
  "description": "string",          // 可选:函数描述
  "nodes": [NodeSchema],            // 必需:节点定义
  "edges": [Edge],                  // 必需:连线定义
  "inputs": [InputDef],             // 可选:输入参数定义
  "outputs": [OutputDef]            // 可选:输出定义
}

响应 (200):

{
  "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):

{
  "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):

{
  "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() 调用中的浅拷贝问题

调试命令

# 查看后端函数列表
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.tsAppState 接口和实现
    • 验证 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
状态: 生产就绪