Demo 案例研究

前面三节讲了通用的最佳实践。本节把镜头对准 `demo/` 目录——它是 rust-agent-flow 官方维护的「最小完整集成」范本,把数据驱动、扩展点注入、工具栏定制、流程控制结构(条件分支 + 循环回环)全部串了起来。读懂 Demo,你就掌握了把框架用起来的全部要素。

Demo 的叙事流程

Demo 默认加载的 `AgentFlow` 数据源展示了一个图灵完备的控制流:

Start ──→ Planner(规划)
              │
              ▼
         Condition(条件判断)
          ├── if_0 → Search(检索)   ┐
          ├── if_1 → Notify(通知)   ├── 三路汇合
          └── else → ToolCall(工具) ┘
                        │
                        ▼
                      Loop(循环)
                   ┌────┴────┐
              loop_body    done
                   │         │
                   ▼         ▼
              Process     Summarize ──→ End
              (循环体)        │
                   │          │
                   └──loop_in──┘  (回环边)

这条流程覆盖了框架的所有关键结构:顺序、条件分支(含 else 兜底)、多路汇合、循环体(含 loop_body 出边与 loop_in 回环边)、循环出口(done)。能正确渲染与编辑它,就证明集成完整。

数据驱动:流程即 JSON

三个预置流程以 JSON 形式存在 `demo/data/`,编译期 `include_str!` 嵌入:

const AGENT_FLOW_JSON: &str    = include_str!("../data/agent_flow.json");
const DATA_PIPELINE_JSON: &str = include_str!("../data/data_pipeline.json");
const SIMPLE_FLOW_JSON: &str   = include_str!("../data/simple_flow.json");

`DemoDataSource` 枚举统一管理:

#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum DemoDataSource {
    #[default]
    AgentFlow,      // 条件分支 + 循环回环(默认)
    DataPipeline,   // 数据清洗 → 分流 → 处理 → 汇合
    SimpleFlow,     // Start → Action → End 线性流程
}

impl DemoDataSource {
    pub fn to_graph(&self) -> FlowGraph {
        FlowGraph::from_document(&self.to_document())
    }
    pub fn to_document(&self) -> FlowDocument {
        let json = match self { ... };
        serde_json::from_str(json).expect("内置 JSON 解析失败")
    }
    pub fn label(&self, lang: Language) -> &'static str { ... }
    pub fn all() -> &'static [DemoDataSource] { ... }
}

数据驱动的收益:新增流程只需加一个 JSON 文件 + 一个枚举变体,**零渲染代码**。节点/边定义与渲染逻辑完全解耦——这是 rust-agent-flow 设计哲学在 Demo 层的体现。

main.rs:装配四步

Demo 的 `main.rs` 是集成骨架的范本,正好对应上一节讲的四步:

fn main() {
    gpui_platform::application()
        .with_assets(CombinedAssets)
        .run(move |cx: &mut gpui::App| {
            // 第1步:框架初始化
            rust_agent_flow_gpui::init(cx);

            cx.spawn(async move |cx| {
                cx.open_window(gpui::WindowOptions::default(), |window, cx| {
                    // 第2步:数据驱动建图
                    let initial_ds = DemoDataSource::AgentFlow;
                    let graph = initial_ds.to_graph();

                    let view = cx.new(|cx| {
                        let mut editor = FlowEditorView::new(graph, cx);
                        // 第3步:自动布局
                        editor.auto_layout(cx);
                        // 第4步:注入扩展(累积型,两个 provider)
                        editor.add_toolbar_provider(
                            Arc::new(DataSourceToolbar::new(initial_ds)), cx,
                        );
                        editor.add_toolbar_provider(
                            Arc::new(AppControlsToolbar::new()), cx,
                        );
                        editor
                    });
                    cx.new(|cx| gpui_component::Root::new(view, window, cx))
                }).expect("Failed to open window");
            }).detach();
        });
}

注意 `gpui_component::Root::new(view, window, cx)` 这层包裹——它为 Button/DropdownMenu 等组件提供主题与焦点上下文,漏了会导致组件样式异常、菜单不响应。这是 gpui-component 的集成要求,非 rust-agent-flow 特有。

DataSourceToolbar:数据源切换

`DataSourceToolbar` 是 `ToolbarProvider` 的典型实现,持有当前数据源状态,切换时 `set_graph` 重建图:

pub struct DataSourceToolbar {
    current: Arc<Mutex<DemoDataSource>>,
}

impl ToolbarProvider for DataSourceToolbar {
    fn render_items(&self, ctx: &ToolbarCtx) -> Vec<AnyElement> {
        let current = *self.current.lock().unwrap();
        let entity = ctx.entity.clone();
        let current_state = self.current.clone();

        Button::new("demo-data-source")
            .icon(IconName::ALargeSmall).small().ghost()
            .tooltip(t(ctx.language, TKey::TbDataSource))
            .dropdown_menu(move |menu, _, _| {
                let mut menu = menu;
                for &ds in DemoDataSource::all() {
                    let label = ds.label(lang);
                    let entity = entity.clone();
                    let st = current_state.clone();
                    menu = menu.item(
                        PopupMenuItem::new(label)
                            .checked(ds == current)
                            .on_click(move |_, _, cx| {
                                *st.lock().unwrap() = ds;        // 更新自身状态
                                let graph = ds.to_graph();
                                entity.update(cx, |this, cx| this.set_graph(graph, cx));
                            }),
                    );
                }
                menu
            })
            .into();
        vec![btn]
    }
}

要点:

- `Arc<Mutex<...>>` 让多份闭包共享可变状态(DropdownMenu 的每个菜单项闭包都要读写 current) - 切换时先更新自身 `current`,再 `set_graph`——保证下次渲染时 `checked` 标记正确 - `entity.update(cx, |this, cx| this.set_graph(graph, cx))` 是 provider 操作编辑器的标准姿势:通过 `ToolbarCtx.entity` 句柄调编辑器方法

AppControlsToolbar:能力与呈现分离

`AppControlsToolbar` 展示「框架提供能力、调用方决定呈现」的分工典范。三个按钮分别调三个框架能力方法:

| 按钮 | 调用 | 框架能力 | |------|------|----------| | 拖拽开关 | `toggle_drag(cx)` | 切换是否允许拖拽节点 | | 主题切换 | `toggle_theme(cx)` | 切换亮/暗主题 + 三重同步 | | 语言切换 | `toggle_language(cx)` | 切换中/英文 + 重建面板 |

// 拖拽开关:selected 态随 ctx.drag_enabled
Button::new("app-toggle-drag")
    .icon(FlowIcon::Drag).small().ghost()
    .selected(ctx.drag_enabled)
    .tooltip(t(lang, TKey::TbToggleDrag))
    .on_click(move |_, _, cx| {
        drag_entity.update(cx, |this, cx| this.toggle_drag(cx));
    });

// 主题切换:图标随当前主题变(暗色显太阳=切到亮色)
Button::new("app-toggle-theme")
    .icon(if ctx.theme.is_dark { IconName::Sun } else { IconName::Moon })
    .on_click(move |_, _, cx| {
        theme_entity.update(cx, |this, cx| this.toggle_theme(cx));
    });

// 语言切换
Button::new("app-toggle-language")
    .icon(IconName::Globe)
    .on_click(move |_, _, cx| {
        lang_entity.update(cx, |this, cx| this.toggle_language(cx));
    });

框架从不规定「拖拽开关按钮放哪」「主题按钮用什么图标」——它只暴露 `toggle_*` 能力方法。Demo 把它们做成三个 ghost Button 放在工具栏末尾,你的应用完全可以换成菜单项、快捷键、侧边栏开关。这是扩展点精神的延伸:**能力在框架、呈现归调用方**。

扩展点空位:DemoDataTypeProvider

Demo 的 `DemoDataTypeProvider` 当前返回空 vec:

pub struct DemoDataTypeProvider;
impl IDataTypeProvider for DemoDataTypeProvider {
    fn data_types(&self) -> Vec<Box<dyn IDataType>> { vec![] }
}

它甚至没被 `main.rs` 注入——因为三个预置流程的参数/变量都用内置类型(String/Integer/Float/Boolean/DateTime/Dynamic)。这个文件存在的意义是**扩展点参考**:告诉你要加自定义类型时该写什么。`main.rs` 也没调用 `set_syntax_service`——默认的 rhai→rust 近似高亮已够用。

这传达了一个重要信息:**扩展点是可选的**。不注入任何扩展,框架用内置默认值就能完整工作;需要时再按需注入。Demo 的「最小注入」姿态(只注入两个 toolbar provider)正是这一理念的示范。

控制流结构的验证

Demo 的 AgentFlow 流程是框架控制流能力的「冒烟测试」:

| 结构 | 验证点 | |------|--------| | 顺序边 | Start→Planner 直连渲染 | | 条件分支 | Condition 的 if_0/if_1/else 三个出端口,三条边分别从不同端口出 | | 多路汇合 | Search/Notify/ToolCall 三条边汇入 Loop 同一入端口 | | 循环体 | Loop 的 loop_body 出边指向 Process,Process 被归入 Loop 的 body_group | | 循环回环 | Process 的边以 `loop_in` 为 target_port、`EdgeKind::LoopBack` 标记,渲染为回环边 | | 循环出口 | Loop 的 done 出边指向 Summarize,离开循环体 | | 折叠 | 收起 Loop 时 body_group 节点隐藏、回环边消失、collapse_pill 显示「已收起」 |

能在 Demo 里看到这些结构正确渲染与交互,就证明你的集成无误。这也是为什么默认数据源是 AgentFlow 而非最简单的 SimpleFlow——它一次性覆盖最多结构。

交互能力一览

Demo 同时是交互能力的演示场:

- 中键拖拽:平移视口 - 滚轮:以鼠标位置为锚点缩放 - 左键拖拽节点:移动节点(受 `toggle_drag` 控制) - 左键从出端口拖到入端口:创建连线 - 点击节点:显示右侧属性面板(schema 驱动或 Start 专属) - 点击边中点「+」:弹出节点选择面板,选类型后插入到边中间 - 工具栏:缩放、适应视图、布局方向、边类型、点阵、拖拽开关、主题、语言、数据源切换

这些交互全部由框架内置,Demo 无需写一行交互代码——只需把 `FlowEditorView` 放进窗口。

从 Demo 到你的项目

把 Demo 改造成自己的项目,路径清晰:

1. **替换数据源**:删掉 `data/*.json` 和 `DemoDataSource`,换成你的 `FlowDocument` 来源(文件/网络/动态构建) 2. **替换工具栏**:把 `DataSourceToolbar`/`AppControlsToolbar` 换成你的业务工具栏,或保留应用控件 3. **按需注入扩展**:有领域类型就实现 `IDataTypeProvider` 并 `set_data_type_provider`;要精确高亮就实现 `SyntaxService` 并 `set_syntax_service` 4. **按需自定义节点**:内置 8 种不够时实现 `IFlowNode`,schema 能表达的优先用 schema 驱动面板 5. **保留装配骨架**:`init` → `new` → `auto_layout` → 注入扩展 → `Root::new` 这条主轴不变

Demo 的价值不在「能跑」,而在它用最少的代码展示了「正确集成的样子」——数据驱动、扩展按需、装配清晰、能力与呈现分离。照这套骨架走,你的项目就站在了框架设计的正轨上。

小结

Demo 用 `AgentFlow` 数据源一次性验证了顺序、条件分支、多路汇合、循环体、回环边、折叠等全部控制流结构。`main.rs` 的四步装配(init/new/auto_layout/注入)是集成骨架范本;`DataSourceToolbar` 展示 provider 操作编辑器的标准姿势;`AppControlsToolbar` 示范能力与呈现分离;空的 `DemoDataTypeProvider` 提示扩展点的可选性。把 Demo 当模板,替换数据与扩展,即得你自己的流程编辑器。

全书完。