485 lines
12 KiB
Markdown
485 lines
12 KiB
Markdown
|
|
# TraceStudio 节点开发指南 (v2.0)
|
|||
|
|
|
|||
|
|
## 🎯 核心设计理念
|
|||
|
|
|
|||
|
|
**让节点开发者专注于业务逻辑,框架自动处理元数据收集和注册。**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📦 快速开始
|
|||
|
|
|
|||
|
|
### 1. 最简单的节点
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from server.app.core.node_base import TraceNode, input_port, output_port, param
|
|||
|
|
from server.app.core.node_registry import register_node
|
|||
|
|
|
|||
|
|
@register_node
|
|||
|
|
class AddNode(TraceNode):
|
|||
|
|
CATEGORY = "Math"
|
|||
|
|
DISPLAY_NAME = "加法"
|
|||
|
|
|
|||
|
|
@input_port("a", "Number", description="加数 A")
|
|||
|
|
@input_port("b", "Number", description="加数 B")
|
|||
|
|
@output_port("result", "Number", description="和")
|
|||
|
|
@param("offset", "Number", default=0, description="偏移量")
|
|||
|
|
def process(self, inputs, context=None):
|
|||
|
|
a = inputs["a"]
|
|||
|
|
b = inputs["b"]
|
|||
|
|
offset = self.get_param("offset", 0)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"outputs": {"result": a + b + offset},
|
|||
|
|
"context": {}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**就这么简单!** 装饰器会自动收集所有属性到 `InputSpec`、`OutputSpec`、`ParamSpec`。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🧩 四大属性规范
|
|||
|
|
|
|||
|
|
### InputSpec - 主输入端口
|
|||
|
|
```python
|
|||
|
|
@input_port(
|
|||
|
|
"data", # 端口名称
|
|||
|
|
"DataTable", # 数据类型
|
|||
|
|
description="输入数据表", # 描述
|
|||
|
|
required=True, # 是否必需(默认 True)
|
|||
|
|
list=False # 是否为列表类型(默认 False)
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**特点:**
|
|||
|
|
- 必须通过连线获得数据
|
|||
|
|
- 不会显示在参数面板中
|
|||
|
|
- 位于节点左侧
|
|||
|
|
|
|||
|
|
### OutputSpec - 主输出端口
|
|||
|
|
```python
|
|||
|
|
@output_port(
|
|||
|
|
"filtered", # 端口名称
|
|||
|
|
"DataTable", # 数据类型
|
|||
|
|
description="过滤后的数据", # 描述
|
|||
|
|
list=False # 是否输出列表
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**特点:**
|
|||
|
|
- 供下游节点连接
|
|||
|
|
- 位于节点右侧
|
|||
|
|
- 支持多输出(复合节点)
|
|||
|
|
|
|||
|
|
### ParamSpec - 控制参数
|
|||
|
|
```python
|
|||
|
|
@param(
|
|||
|
|
"threshold", # 参数名
|
|||
|
|
"Number", # 参数类型
|
|||
|
|
default=0.5, # 默认值
|
|||
|
|
description="阈值", # 描述
|
|||
|
|
widget="slider", # 控件类型
|
|||
|
|
min=0, max=1, step=0.1 # 数值限制
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**支持的参数类型和控件:**
|
|||
|
|
|
|||
|
|
| 类型 | widget | 配置项 | 示例 |
|
|||
|
|
|------|--------|--------|------|
|
|||
|
|
| Number | `slider` | min, max, step | `@param("value", "Number", default=0.5, widget="slider", min=0, max=1, step=0.1)` |
|
|||
|
|
| String | `text` | multiline | `@param("text", "String", default="", multiline=False)` |
|
|||
|
|
| String | `file` | - | `@param("path", "String", widget="file")` |
|
|||
|
|
| String | `dropdown` | options | `@param("mode", "String", widget="dropdown", options=["A", "B", "C"])` |
|
|||
|
|
| Boolean | `checkbox` | - | `@param("enabled", "Boolean", default=True)` |
|
|||
|
|
|
|||
|
|
### ContextSpec - 上下文变量
|
|||
|
|
```python
|
|||
|
|
@context_var(
|
|||
|
|
"row_count", # 变量名
|
|||
|
|
"Integer", # 变量类型
|
|||
|
|
description="数据行数" # 描述
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**特点:**
|
|||
|
|
- 节点计算的副产物/元数据
|
|||
|
|
- 自动沿连线向下游广播
|
|||
|
|
- 下游节点可在参数中引用:`$NodeID.row_count`
|
|||
|
|
- 可提升为全局变量:`$Global.row_count`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎨 装饰器详解
|
|||
|
|
|
|||
|
|
### @register_node - 注册节点
|
|||
|
|
```python
|
|||
|
|
@register_node
|
|||
|
|
class MyNode(TraceNode):
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**作用:**
|
|||
|
|
- 自动注册节点到全局注册表
|
|||
|
|
- 自动收集所有 `@input_port`、`@output_port`、`@param`、`@context_var` 装饰器
|
|||
|
|
- 合并到 `InputSpec`、`OutputSpec`、`ParamSpec`、`ContextSpec`
|
|||
|
|
|
|||
|
|
### 装饰器链式调用
|
|||
|
|
```python
|
|||
|
|
@input_port("a", "Number", description="输入A")
|
|||
|
|
@input_port("b", "Number", description="输入B")
|
|||
|
|
@output_port("result", "Number", description="结果")
|
|||
|
|
@param("offset", "Number", default=0)
|
|||
|
|
@context_var("count", "Integer", description="计算次数")
|
|||
|
|
def process(self, inputs, context=None):
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**注意:** 装饰器可以任意顺序,框架会自动分类收集。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📋 节点类型
|
|||
|
|
|
|||
|
|
### 1. 标准流水线节点(1进1出)
|
|||
|
|
```python
|
|||
|
|
@register_node
|
|||
|
|
class FilterNode(TraceNode):
|
|||
|
|
NODE_TYPE = NodeType.NORMAL # 默认值
|
|||
|
|
|
|||
|
|
@input_port("data", "DataTable")
|
|||
|
|
@output_port("filtered", "DataTable")
|
|||
|
|
def process(self, inputs, context=None):
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 输入节点(仅输出)
|
|||
|
|
```python
|
|||
|
|
@register_node
|
|||
|
|
class LoaderNode(TraceNode):
|
|||
|
|
NODE_TYPE = NodeType.INPUT
|
|||
|
|
|
|||
|
|
# 无 @input_port
|
|||
|
|
@output_port("data", "DataTable")
|
|||
|
|
def process(self, inputs, context=None):
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 输出节点(仅输入)
|
|||
|
|
```python
|
|||
|
|
@register_node
|
|||
|
|
class DisplayNode(TraceNode):
|
|||
|
|
NODE_TYPE = NodeType.OUTPUT
|
|||
|
|
|
|||
|
|
@input_port("data", "DataTable")
|
|||
|
|
# 无 @output_port
|
|||
|
|
def process(self, inputs, context=None):
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 聚合节点(多输入)
|
|||
|
|
```python
|
|||
|
|
@register_node
|
|||
|
|
class ConcatNode(TraceNode):
|
|||
|
|
NODE_TYPE = NodeType.COMPOSITE
|
|||
|
|
|
|||
|
|
@input_port("tables", "DataTable", list=True) # 列表输入
|
|||
|
|
@output_port("concatenated", "DataTable")
|
|||
|
|
def process(self, inputs, context=None):
|
|||
|
|
tables = inputs["tables"] # 这是一个列表
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔄 process 方法规范
|
|||
|
|
|
|||
|
|
### 方法签名
|
|||
|
|
```python
|
|||
|
|
def process(self, inputs: Dict[str, Any], context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 参数说明
|
|||
|
|
- **inputs**: 主输入数据字典,键为端口名,值为数据
|
|||
|
|
- **context**: 上下文字典
|
|||
|
|
- 全局变量:`context["$Global.variable_name"]`
|
|||
|
|
- 上游节点变量:`context["$NodeID.variable_name"]`
|
|||
|
|
|
|||
|
|
### 返回值格式
|
|||
|
|
|
|||
|
|
**标准格式(推荐):**
|
|||
|
|
```python
|
|||
|
|
return {
|
|||
|
|
"outputs": {"port_name": data}, # 对应 OutputSpec
|
|||
|
|
"context": {"var_name": value} # 对应 ContextSpec
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**简化格式(仅输出):**
|
|||
|
|
```python
|
|||
|
|
return {"result": data} # 自动转换为 {"outputs": {"result": data}, "context": {}}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 💾 缓存策略
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
@register_node
|
|||
|
|
class ExpensiveNode(TraceNode):
|
|||
|
|
CACHE_POLICY = CachePolicy.MEMORY # 或 DISK 或 NONE
|
|||
|
|
|
|||
|
|
def process(self, inputs, context=None):
|
|||
|
|
# 计算密集型操作
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**缓存键组成:**
|
|||
|
|
- 节点类名
|
|||
|
|
- 参数值
|
|||
|
|
- 输入数据(哈希)
|
|||
|
|
- 上下文(哈希)
|
|||
|
|
|
|||
|
|
**注意:** 相同输入和参数会直接返回缓存结果,不重复计算。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔍 预览模式
|
|||
|
|
|
|||
|
|
### 默认实现
|
|||
|
|
```python
|
|||
|
|
def preview(self, inputs, context=None, limit=10):
|
|||
|
|
# 默认:调用 process 然后截取数据
|
|||
|
|
result = self.process(inputs, context)
|
|||
|
|
return self._truncate_preview_data(result, limit)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 自定义优化
|
|||
|
|
```python
|
|||
|
|
@register_node
|
|||
|
|
class BigDataNode(TraceNode):
|
|||
|
|
def preview(self, inputs, context=None, limit=10):
|
|||
|
|
# 优化:仅处理前 N 行
|
|||
|
|
data = inputs["data"].head(limit)
|
|||
|
|
return self.process({"data": data}, context)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📊 完整示例
|
|||
|
|
|
|||
|
|
### 示例:CSV 过滤器
|
|||
|
|
```python
|
|||
|
|
@register_node
|
|||
|
|
class AdvancedFilterNode(TraceNode):
|
|||
|
|
"""高级数据过滤器"""
|
|||
|
|
|
|||
|
|
CATEGORY = "Data/Transform"
|
|||
|
|
DISPLAY_NAME = "高级过滤"
|
|||
|
|
DESCRIPTION = "支持多条件过滤的数据节点"
|
|||
|
|
ICON = "🔍"
|
|||
|
|
CACHE_POLICY = CachePolicy.MEMORY
|
|||
|
|
|
|||
|
|
@input_port("table", "DataTable", description="输入数据表", required=True)
|
|||
|
|
@output_port("filtered", "DataTable", description="过滤后的数据")
|
|||
|
|
@output_port("removed", "DataTable", description="被移除的数据")
|
|||
|
|
|
|||
|
|
@param("column", "String", default="", description="过滤列名")
|
|||
|
|
@param("operator", "String", default=">", widget="dropdown",
|
|||
|
|
options=[">", ">=", "<", "<=", "==", "!="], description="运算符")
|
|||
|
|
@param("value", "Number", default=0, description="比较值")
|
|||
|
|
@param("return_removed", "Boolean", default=False, description="输出被移除的数据")
|
|||
|
|
|
|||
|
|
@context_var("filtered_count", "Integer", description="保留行数")
|
|||
|
|
@context_var("removed_count", "Integer", description="移除行数")
|
|||
|
|
@context_var("filter_expression", "String", description="过滤表达式")
|
|||
|
|
|
|||
|
|
def process(self, inputs, context=None):
|
|||
|
|
import pandas as pd
|
|||
|
|
|
|||
|
|
table = inputs["table"]
|
|||
|
|
column = self.get_param("column")
|
|||
|
|
operator = self.get_param("operator", ">")
|
|||
|
|
value = self.get_param("value", 0)
|
|||
|
|
return_removed = self.get_param("return_removed", False)
|
|||
|
|
|
|||
|
|
# 应用过滤
|
|||
|
|
if operator == ">":
|
|||
|
|
mask = table[column] > value
|
|||
|
|
elif operator == ">=":
|
|||
|
|
mask = table[column] >= value
|
|||
|
|
elif operator == "<":
|
|||
|
|
mask = table[column] < value
|
|||
|
|
elif operator == "<=":
|
|||
|
|
mask = table[column] <= value
|
|||
|
|
elif operator == "==":
|
|||
|
|
mask = table[column] == value
|
|||
|
|
else: # !=
|
|||
|
|
mask = table[column] != value
|
|||
|
|
|
|||
|
|
filtered = table[mask]
|
|||
|
|
removed = table[~mask]
|
|||
|
|
|
|||
|
|
outputs = {"filtered": filtered}
|
|||
|
|
if return_removed:
|
|||
|
|
outputs["removed"] = removed
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"outputs": outputs,
|
|||
|
|
"context": {
|
|||
|
|
"filtered_count": len(filtered),
|
|||
|
|
"removed_count": len(removed),
|
|||
|
|
"filter_expression": f"{column} {operator} {value}"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ✅ 最佳实践
|
|||
|
|
|
|||
|
|
### 1. 命名规范
|
|||
|
|
- **类名**:使用 `XxxNode` 后缀(如 `FilterRowsNode`)
|
|||
|
|
- **端口名**:小写字母 + 下划线(如 `input_data`)
|
|||
|
|
- **参数名**:小写字母 + 下划线(如 `max_value`)
|
|||
|
|
|
|||
|
|
### 2. 分类路径
|
|||
|
|
```python
|
|||
|
|
CATEGORY = "Data/Transform/Filter" # 多级分类
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 描述文档
|
|||
|
|
- `DESCRIPTION`:简短说明(1-2 句话)
|
|||
|
|
- 端口 `description`:说明数据用途
|
|||
|
|
- 参数 `description`:说明参数含义
|
|||
|
|
|
|||
|
|
### 4. 错误处理
|
|||
|
|
```python
|
|||
|
|
def process(self, inputs, context=None):
|
|||
|
|
try:
|
|||
|
|
# 处理逻辑
|
|||
|
|
...
|
|||
|
|
except Exception as e:
|
|||
|
|
raise ValueError(f"节点执行失败: {e}")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5. 类型提示
|
|||
|
|
```python
|
|||
|
|
def process(self, inputs: Dict[str, Any], context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🚀 部署自定义节点
|
|||
|
|
|
|||
|
|
### 文件位置
|
|||
|
|
```
|
|||
|
|
TraceStudio/
|
|||
|
|
├── cloud/
|
|||
|
|
│ └── custom_nodes/ # 用户自定义节点目录
|
|||
|
|
│ └── my_nodes.py # 你的节点文件
|
|||
|
|
└── server/
|
|||
|
|
└── app/
|
|||
|
|
└── nodes/
|
|||
|
|
└── example_nodes.py # 内置节点示例
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 自动加载
|
|||
|
|
服务器启动时会自动扫描 `cloud/custom_nodes/` 目录,加载所有带 `@register_node` 的节点。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🧪 测试节点
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd server
|
|||
|
|
python tests/test_node_system.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**测试内容:**
|
|||
|
|
- ✅ 节点注册
|
|||
|
|
- ✅ 元数据生成
|
|||
|
|
- ✅ 节点执行
|
|||
|
|
- ✅ 输入验证
|
|||
|
|
- ✅ 多输入聚合
|
|||
|
|
- ✅ 缓存机制
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎓 对比:旧版 vs 新版
|
|||
|
|
|
|||
|
|
### 旧版方式(繁琐)
|
|||
|
|
```python
|
|||
|
|
class OldNode(TraceNode):
|
|||
|
|
InputSpec = {
|
|||
|
|
"data": ("DataTable", {"description": "输入数据", "required": True})
|
|||
|
|
}
|
|||
|
|
OutputSpec = {
|
|||
|
|
"result": ("DataTable", {"description": "输出结果"})
|
|||
|
|
}
|
|||
|
|
ParamSpec = {
|
|||
|
|
"threshold": ("Number", {"default": 0.5, "min": 0, "max": 1})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def process(self, inputs, context=None):
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 新版方式(简洁)
|
|||
|
|
```python
|
|||
|
|
@register_node
|
|||
|
|
class NewNode(TraceNode):
|
|||
|
|
@input_port("data", "DataTable", description="输入数据")
|
|||
|
|
@output_port("result", "DataTable", description="输出结果")
|
|||
|
|
@param("threshold", "Number", default=0.5, min=0, max=1)
|
|||
|
|
def process(self, inputs, context=None):
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**优势:**
|
|||
|
|
- ✅ 代码更简洁(减少 40% 代码量)
|
|||
|
|
- ✅ 语义更清晰(装饰器即文档)
|
|||
|
|
- ✅ 易于维护(属性定义靠近使用位置)
|
|||
|
|
- ✅ 自动注册(无需手动调用)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📚 进阶主题
|
|||
|
|
|
|||
|
|
### 动态参数选项
|
|||
|
|
```python
|
|||
|
|
def get_column_options(self, upstream_data):
|
|||
|
|
"""动态获取列名选项"""
|
|||
|
|
if "table" in upstream_data:
|
|||
|
|
return list(upstream_data["table"].columns)
|
|||
|
|
return []
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 条件输出
|
|||
|
|
```python
|
|||
|
|
def process(self, inputs, context=None):
|
|||
|
|
outputs = {"main": result}
|
|||
|
|
|
|||
|
|
if self.get_param("debug_mode"):
|
|||
|
|
outputs["debug_info"] = debug_data
|
|||
|
|
|
|||
|
|
return {"outputs": outputs, "context": {}}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 上下文引用
|
|||
|
|
```python
|
|||
|
|
def process(self, inputs, context=None):
|
|||
|
|
# 引用全局变量
|
|||
|
|
user_name = context.get("$Global.user_name", "guest")
|
|||
|
|
|
|||
|
|
# 引用上游节点变量
|
|||
|
|
upstream_count = context.get("$Loader_1.row_count", 0)
|
|||
|
|
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
祝你开发愉快!🎉
|