流程图数据模型
三层抽象
rust-agent-flow 的图模型由三层抽象构成:
FlowGraph(图容器)
├─ Node(节点) ← NodeId (slotmap key)
│ └─ NodeData = serde_json::Value(业务数据)
└─ Edge(边) ← EdgeId (slotmap key)
├─ source / target: NodeId
├─ source_port / target_port: Option<PortId>
├─ edge_type: Bezier/Straight/Step/SmoothStep
└─ kind: Normal/LoopBack
FlowGraph:图容器
`FlowGraph` 用两个 `SlotMap` 存储节点和边,外加版本计数器:
pub struct FlowGraph {
nodes: SlotMap<NodeId, Node>,
edges: SlotMap<EdgeId, Edge>,
version: u64, // 结构变更时递增
}
**slotmap 的意义**:节点/边的 ID 是稳定键,删除一个节点不会使其他节点的 ID 失效(对比 Vec 索引会移位)。这保证了 `NodeId`/`EdgeId` 在整个生命周期可安全传递与缓存。
**版本计数器**:任何结构性变更(增删节点/边、修改位置)都会 `version = wrapping_add(1)`。用于失效缓存的几何数据(如 `PortResolver`),避免使用过期的端口位置。
Node:节点
pub struct Node {
pub id: NodeId,
pub kind: NodeKind, // String,匹配 IFlowNode
pub data: NodeData, // serde_json::Value
pub position: PointF, // 逻辑坐标左上角
pub size: SizeF,
}
- `kind` 是 `String` 而非枚举——支持任意自定义节点类型,无需改 core - `data` 是自由 JSON——具体字段由 `NodeSchema.fields` 声明约束 - `position`/`size` 在布局后由 dagre 填充
Edge:边
pub struct Edge {
pub id: EdgeId,
pub source: NodeId,
pub source_port: Option<PortId>,
pub target: NodeId,
pub target_port: Option<PortId>,
pub edge_type: EdgeType, // Bezier/Straight/Step/SmoothStep
pub kind: EdgeKind, // Normal/LoopBack
}
端口是**可选的**——`None` 表示用默认端口(框架自动推导方向)。结构化节点(Condition/Loop)必须指定端口:
// Condition 的 if_0 出口
edge.source_port = Some("if_0".to_string());
// Loop 的回环边
edge.target_port = Some("loop_in".to_string());
edge.kind = EdgeKind::LoopBack;
Port:端口
端口通过 `PortSpec` 声明(在 NodeSchema 中),运行时用 `PortId`(String)引用:
pub struct PortSpec {
pub id: PortId, // String
pub direction: PortDirection, // In / Out
pub side: PortSide, // Top/Right/Bottom/Left/Auto
pub label: Option<String>,
}
`PortSide::Auto` 让框架根据节点相对位置自动推导方向——大多数节点只需声明 Auto。
数据与视图的分离
图模型只存数据,**不含渲染逻辑**。渲染由 gpui 层的 `IFlowNode::get_view` 负责:
Node (data) ──→ IFlowNode (by kind) ──→ AnyElement (view)
这意味着:
- 同一图可被不同渲染层呈现(如未来的 egui 版本) - 节点视图逻辑变更不影响图数据 - 图数据可序列化,视图不可
增删改查 API
// 节点
graph.add_node(kind, data) -> NodeId
graph.add_node_with_size(kind, data, size) -> NodeId
graph.remove_node(id) -> Option<Node> // 同时删除关联边
graph.node(id) -> Option<&Node>
graph.node_mut(id) -> Option<&mut Node> // 触发 version++
graph.nodes() -> impl Iterator<Item = &Node>
// 边
graph.add_edge(edge) -> EdgeId
graph.remove_edge(id) -> Option<Edge>
graph.out_edges(node) -> impl Iterator<Item = &Edge>
graph.in_edges(node) -> impl Iterator<Item = &Edge>
`remove_node` 会**自动清除关联边**(retain 过滤 source/target 匹配的边),保证图一致性。
循环体分组
`loop_body_groups()` 是 Loop 节点的核心算法——BFS 展开循环体节点集合:
pub fn loop_body_groups(&self) -> HashMap<NodeId, HashSet<NodeId>> {
// 1. 找所有 loop_body 出边,按 Loop 节点分组
// 2. BFS 沿前向边展开,排除:
// - loop_in 回环边(target_port == "loop_in")
// - 回到 Loop 节点本身的边(如 done)
// - done 目标节点(防止吸收出口节点)
}
这是渲染层隐藏循环体、回环边路由的**唯一数据源**,在 `relayout` 末尾缓存。
小结
`FlowGraph` 基于 slotmap 稳定键,`Node`/`Edge`/`Port` 三层抽象清晰分离数据与视图。版本计数器驱动缓存失效,`loop_body_groups` 为循环渲染提供单一真相。图模型不含任何渲染逻辑,确保可复用与可序列化。
下一节:[渲染生命周期](render-lifecycle.md)