12 KiB
12 KiB
TraceStudio 前端 v0.3.0 实现总结
发布日期: 2024-01 版本: v0.3.0 关键特性: Param 绑定模式、EdgeType 标记、暴露端口
📋 概述
本版本完成了四大属性(InputSpec/OutputSpec/ParamSpec/ContextSpec)在前端的完整实现,包括:
- Param 绑定模式系统 - 三种参数值来源:静态值、Context 引用、暴露为端口
- 连线 EdgeType 标记 - 数据维度的视觉编码:数组粗线(4px)、标量细线(3px)
- 暴露端口显示 - Param 升级为输入端口、Context 暴露为输出端口
🔧 核心改动
1. 数据结构扩展 (web/src/stores/useStore.ts)
新增类型定义
// Param 绑定模式
export type ParamBindingMode = 'static' | 'context' | 'exposed'
export interface ParamBinding {
mode: ParamBindingMode
value?: any // 静态值(mode='static')
contextRef?: string // 上下文引用(mode='context'),格式: $Global.VarName 或 $NodeID.VarName
}
export interface NodeSchema {
// ... 现有字段 ...
bindings?: Record<string, ParamBinding> // 参数绑定信息
exposedPorts?: {
params: string[] // 暴露为输入端口的参数列表
contexts: string[] // 暴露为输出端口的 Context 变量列表
}
}
用途: 支持参数值动态来源和端口暴露的数据模型
2. API 类型规范化 (web/src/utils/api.ts)
扩展 getPlugins 返回类型
// 从后端 /api/plugins 返回
{
plugins: {
[nodeType: string]: {
display_name: string
// 新增字段
inputs?: Array<{ name: string; type: string; description?: string }>
outputs?: Array<{ name: string; type: string; description?: string }>
param_schema?: Array<{ name: string; type: string; default?: any }>
context_vars?: Record<string, any>
}
}
}
用途: 前端能正确解析并显示四大属性
3. Inspector 参数绑定 UI (web/src/components/Inspector.tsx)
新增功能
// 1. availableContextVars 计算(useMemo)
const availableContextVars = useMemo(() => {
// 收集 $Global.* 变量
// 递归查找上游节点的 context_vars
// 返回可选变量列表
}, [node, nodes, edges])
// 2. 三种绑定模式按钮(inline)
<button onClick={() => handleModeChange('static')}>📝 静态值</button>
<button onClick={() => handleModeChange('context')}>🔗 Context</button>
<button onClick={() => handleModeChange('exposed')}>⚡ 暴露端口</button>
// 3. 模式对应的输入控件
if (binding.mode === 'static') {
// input[number] 或 input[text]
}
if (binding.mode === 'context') {
// select 下拉列表
}
if (binding.mode === 'exposed') {
// 提示文本
}
用户交互流:
- 用户在 Inspector 中选择参数
- 点击三个绑定模式按钮之一
- 根据模式显示对应的输入控件
- 修改时同时更新
node.data[key]和node.bindings[key]
数据流:
用户操作 → handleModeChange() → updateNodeData()
→ node.bindings[paramName] = { mode, value/contextRef }
→ node.exposedPorts.params.push(paramName) [仅 mode='exposed']
4. 连线 EdgeType 标记 (web/src/components/Workspace.tsx)
新增 Helper 函数
function isArrayType(type?: string): boolean {
return type?.includes('Array') || type?.includes('List') || type?.includes('Table')
}
修改 onConnect 处理器
const onConnect = useCallback((connection: Connection) => {
const sourceType = getNodeOutputType(nodes, connection.source, connection.sourceHandle)
// 计算 edgeType
const edgeType = isArrayType(sourceType) ? 'array' : 'scalar'
const strokeWidth = edgeType === 'array' ? 4 : 3
// 添加到 edge.data 用于持久化
return addEdge({
...connection,
style: { strokeWidth },
data: { sourceType, edgeType, color: edgeColor }
}, finalEdges)
}, [nodes, setEdges])
视觉效果:
- 数组边:粗线(strokeWidth 4px)、亮蓝色
- 标量边:细线(strokeWidth 3px)、橙色/紫色
执行动画保留:
useEffect(() => {
// 执行时更新边的动画状态
const updatedEdges = edges.map((edge) => {
const baseStrokeWidth = edge.data?.edgeType === 'array' ? 4 : 3
return {
...edge,
animated: executionStatus.running,
style: {
...edge.style,
strokeWidth: executionStatus.running ? baseStrokeWidth * 1.5 : baseStrokeWidth
}
}
})
}, [executionStatus])
5. UniversalNode 暴露端口显示 (web/src/components/nodes/UniversalNode.tsx)
端口集合构建
// 合并原始和暴露的端口
const allInputs = [
...inputs,
...(exposedPorts.params || []).map(paramName => ({
name: paramName,
type: 'Exposed',
isExposed: true
}))
]
const allOutputs = [
...outputs,
...(exposedPorts.contexts || []).map(contextName => ({
name: contextName,
type: 'Context',
isExposed: true
}))
]
Handle 渲染(样式区分)
{allInputs.map((input, idx) => {
const color = input.isExposed ? '#ec4899' : getTypeColor(input.type)
const size = input.isExposed ? 14 : 12
return (
<Handle
style={{
background: color,
width: size, height: size,
boxShadow: `0 0 8px ${color}80`
}}
title={input.isExposed ? `⚡ Exposed Param: ${input.name}` : input.name}
/>
)
})}
暴露端口指示区(新增)
{(exposedPorts.params?.length > 0 || exposedPorts.contexts?.length > 0) && (
<div style={{ background: 'rgba(236,72,153,0.08)', borderBottom: '1px solid rgba(236,72,153,0.2)' }}>
{exposedPorts.params?.length > 0 && <div>⚡ Params: {exposedPorts.params.join(', ')}</div>}
{exposedPorts.contexts?.length > 0 && <div>📋 Contexts: {exposedPorts.contexts.join(', ')}</div>}
</div>
)}
视觉标记:
- 暴露 Param 端口:粉红色(#ec4899)、14x14、⚡ 标签
- 暴露 Context 端口:绿色(#22c55e)、14x14、📋 标签
- 节点内指示区:紫红色背景,列出所有暴露的端口名称
6. 参数规范化 (web/src/components/NodePalette.tsx)
normalizeParamSchema 函数
function normalizeParamSchema(raw: any): Array<any> {
if (!raw) return []
if (Array.isArray(raw)) return raw
// 对象转数组:Object<name, spec> → Array<{ name, ...spec }>
return Object.entries(raw).map(([name, spec]) => ({ name, ...(spec as any) }))
}
插件加载时应用规范化
const normalized = Object.fromEntries(
Object.entries(response.data.plugins || {}).map(([type, meta]) => [
type,
{
...meta,
param_schema: normalizeParamSchema((meta as any).param_schema),
inputs: (meta as any).inputs || [],
outputs: (meta as any).outputs || []
}
])
)
用途: 确保参数结构统一,前端代码不需分别处理对象和数组格式
📊 功能对应表
| 需求 | 实现位置 | 关键代码 | 状态 |
|---|---|---|---|
| Param 三种绑定模式 | Inspector.tsx | ParamBindingMode type + 模式按钮 | ✅ 完成 |
| Context 变量聚合 | Inspector.tsx | availableContextVars useMemo | ✅ 完成 |
| 端口暴露动作 | Inspector.tsx | handleModeChange + exposedPorts | ✅ 完成 |
| EdgeType 推断 | Workspace.tsx | isArrayType + onConnect | ✅ 完成 |
| 连线粗细差异 | Workspace.tsx | strokeWidth 4/3 映射 | ✅ 完成 |
| 暴露 Param 端口 | UniversalNode.tsx | allInputs 合并 + isExposed 标记 | ✅ 完成 |
| 暴露 Context 端口 | UniversalNode.tsx | allOutputs 合并 + 绿色标记 | ✅ 完成 |
| 端口指示区显示 | UniversalNode.tsx | exposedPorts 检查 + 紫红色区域 | ✅ 完成 |
| 参数规范化 | NodePalette.tsx | normalizeParamSchema 函数 | ✅ 完成 |
🧪 测试场景
场景 1:参数静态值
1. 创建 Transform 节点
2. Inspector 中参数选择 📝 静态值
3. 输入值(数字/文本)
✓ 验证:node.bindings[key] = { mode: 'static', value: ... }
场景 2:Context 绑定
1. Loader → Transform(数据连线)
2. Transform 参数选择 🔗 Context
3. 下拉列表选择 $LoaderNode.Variable
✓ 验证:node.bindings[key] = { mode: 'context', contextRef: '$LoaderNode.Variable' }
场景 3:端口暴露
1. Transform 参数点击 ⚡ 暴露端口
2. 节点左侧出现粉红色新端口
✓ 验证:node.exposedPorts.params 包含该参数名
场景 4:EdgeType 视觉
1. Loader (DataTable 输出) → Transform
2. 连线显示为粗线(4px)
✓ 验证:edge.data.edgeType = 'array'
🔌 集成注意事项
前端与后端通讯
- 现状: bindings 和 exposedPorts 存储在前端,未同步到后端
- TODO: 修改
/api/graph/execute端点接收并处理这些元数据
参数值替换流程(待实现)
1. 用户执行工作流
2. 前端收集所有 bindings
3. 发送到后端 /api/graph/execute { nodes, edges, bindings, exposedPorts }
4. 后端解析 bindings:
- mode='static' → 使用 value
- mode='context' → 从全局/上游节点查询 contextRef
- mode='exposed' → 从上游连线获取值
5. 执行节点时使用最终参数值
暴露端口连接处理(已支持)
1. UniversalNode 显示暴露的端口(粉红/绿色)
2. 用户可拖拽从其他节点连接到暴露端口
3. Workspace.onConnect 正常处理,edge.target 指向暴露的 Handle ID
4. 执行时后端需识别 targetHandle 是否为暴露端口
📝 文件变更清单
| 文件 | 变更类型 | 关键改动 |
|---|---|---|
web/src/stores/useStore.ts |
修改 | 新增 ParamBindingMode、ParamBinding、bindings、exposedPorts |
web/src/utils/api.ts |
修改 | 扩展 getPlugins 返回类型,包含四大属性 |
web/src/components/Inspector.tsx |
修改 | 新增 Param 绑定 UI、availableContextVars、三种模式输入 |
web/src/components/Workspace.tsx |
修改 | 新增 isArrayType、onConnect 中 edgeType 计算 |
web/src/components/nodes/UniversalNode.tsx |
修改 | 新增 allInputs/allOutputs、暴露端口指示区、端口样式差异 |
web/src/components/NodePalette.tsx |
修改 | 新增 normalizeParamSchema、应用规范化 |
web/FEATURES_TEST.md |
新建 | 功能验证清单和测试场景 |
web/TEST_INTEGRATION.js |
新建 | 浏览器控制台测试脚本 |
web/IMPLEMENTATION_SUMMARY.md |
新建 | 本文档 |
🎯 后续工作优先级
🔴 高优先级
- 后端
/api/graph/execute解析 bindings 并替换参数值 - 工作流保存/加载时持久化 bindings 和 exposedPorts
- 删除上游节点时自动清空相关 contextRef
🟡 中优先级
- 连线右键菜单:修改 dimensionMode(EXPAND/COLLAPSE/BROADCAST)
- 参数绑定撤销/重做支持
- Context 变量搜索框(列表过长时)
🟢 低优先级
- 批量修改参数绑定模式
- 暴露端口权限控制
- 性能优化:大规模图的渲染
🚀 快速验证
浏览器控制台测试
// 1. 加载测试脚本
const script = document.createElement('script')
script.src = '/TEST_INTEGRATION.js'
document.head.appendChild(script)
// 2. 运行完整测试
await testCompleteWorkflow()
// 3. 或逐个测试
await testParamBinding()
await testEdgeType()
await testExposedPorts()
📚 相关文档
- FEATURES_TEST.md - 详细测试场景
- TEST_INTEGRATION.js - 控制台测试脚本
- ../docs/web1.0/ - 设计文档存档
版本信息: v0.3.0 | 2024-01 | 前端完整开发