397 lines
12 KiB
Markdown
397 lines
12 KiB
Markdown
# TraceStudio 前端 v0.3.0 实现总结
|
||
|
||
**发布日期**: 2024-01
|
||
**版本**: v0.3.0
|
||
**关键特性**: Param 绑定模式、EdgeType 标记、暴露端口
|
||
|
||
---
|
||
|
||
## 📋 概述
|
||
|
||
本版本完成了四大属性(InputSpec/OutputSpec/ParamSpec/ContextSpec)在前端的完整实现,包括:
|
||
1. **Param 绑定模式系统** - 三种参数值来源:静态值、Context 引用、暴露为端口
|
||
2. **连线 EdgeType 标记** - 数据维度的视觉编码:数组粗线(4px)、标量细线(3px)
|
||
3. **暴露端口显示** - Param 升级为输入端口、Context 暴露为输出端口
|
||
|
||
---
|
||
|
||
## 🔧 核心改动
|
||
|
||
### 1. 数据结构扩展 (`web/src/stores/useStore.ts`)
|
||
|
||
#### 新增类型定义
|
||
```typescript
|
||
// 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 返回类型
|
||
```typescript
|
||
// 从后端 /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`)
|
||
|
||
#### 新增功能
|
||
```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') {
|
||
// 提示文本
|
||
}
|
||
```
|
||
|
||
**用户交互流**:
|
||
1. 用户在 Inspector 中选择参数
|
||
2. 点击三个绑定模式按钮之一
|
||
3. 根据模式显示对应的输入控件
|
||
4. 修改时同时更新 `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 函数
|
||
```typescript
|
||
function isArrayType(type?: string): boolean {
|
||
return type?.includes('Array') || type?.includes('List') || type?.includes('Table')
|
||
}
|
||
```
|
||
|
||
#### 修改 onConnect 处理器
|
||
```typescript
|
||
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)、橙色/紫色
|
||
|
||
**执行动画保留**:
|
||
```typescript
|
||
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`)
|
||
|
||
#### 端口集合构建
|
||
```typescript
|
||
// 合并原始和暴露的端口
|
||
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 渲染(样式区分)
|
||
```tsx
|
||
{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}
|
||
/>
|
||
)
|
||
})}
|
||
```
|
||
|
||
#### 暴露端口指示区(新增)
|
||
```tsx
|
||
{(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 函数
|
||
```typescript
|
||
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) }))
|
||
}
|
||
```
|
||
|
||
#### 插件加载时应用规范化
|
||
```typescript
|
||
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 变量搜索框(列表过长时)
|
||
|
||
### 🟢 低优先级
|
||
- [ ] 批量修改参数绑定模式
|
||
- [ ] 暴露端口权限控制
|
||
- [ ] 性能优化:大规模图的渲染
|
||
|
||
---
|
||
|
||
## 🚀 快速验证
|
||
|
||
### 浏览器控制台测试
|
||
```javascript
|
||
// 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](./FEATURES_TEST.md) - 详细测试场景
|
||
- [TEST_INTEGRATION.js](./TEST_INTEGRATION.js) - 控制台测试脚本
|
||
- [../docs/web1.0/](../docs/web1.0/) - 设计文档存档
|
||
|
||
---
|
||
|
||
**版本信息**: v0.3.0 | 2024-01 | 前端完整开发
|