基于 Spring AI + EasyUI 模拟实现类 ComfyUI 风格的大模型工作流 Demo(核心是节点拖拽编排节点参数配置流程执行链路可视化多节点串联调用大模型),我会提供最小功能实现的可运行代码,聚焦 ComfyUI 的核心特性:节点画布拖拽 + 节点参数配置 + 流程链路执行 + 执行结果可视化

一、Demo 核心设计(类 ComfyUI 最小功能)

ComfyUI 核心特性

本 Demo 实现方式

节点画布拖拽

EasyUI + HTML5 拖拽 API 实现节点拖拽、连线(简化版)

节点参数配置

弹窗配置节点参数(如 Prompt 模板、模型选择)

流程链路执行

Spring AI 按节点顺序调用大模型,传递中间结果

执行结果可视化

页面展示每个节点的输入/输出、执行状态

节点类型

仅保留核心节点:Prompt 节点、LLM 调用节点、输出节点

二、完整代码实现
1. 依赖配置(pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>springai-comfyui-demo</artifactId>
    <version>1.0.0</version>
    <name>SpringAI-ComfyUI-Demo</name>

    <dependencies>
        <!-- Spring Web + Thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- Spring AI 核心 + OpenAI -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
            <version>1.0.1</version>
        </dependency>

        <!-- JSON 处理 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.7.18</version>
            </plugin>
        </plugins>
    </build>
</project>
2. 配置文件(application.yml)
server:
  port: 8080

spring:
  # Thymeleaf 配置
  thymeleaf:
    cache: false
  # Spring AI + OpenAI 配置(替换为你的API Key)
  ai:
    openai:
      api-key: your-openai-api-key
      chat:
        options:
          model: gpt-3.5-turbo
          temperature: 0.2

# 日志配置
logging:
  level:
    com.example.demo: DEBUG
3. 核心模型定义(类 ComfyUI 节点/流程)
3.1 节点基类(ComfyNode.java)
package com.example.demo.model;

import lombok.Data;

import java.util.Map;

/**
 * ComfyUI 节点基类(最小功能)
 */
@Data
public class ComfyNode {
    /** 节点ID(唯一) */
    private String nodeId;
    /** 节点类型(prompt/llm/in_output) */
    private String nodeType;
    /** 节点名称 */
    private String nodeName;
    /** 节点位置(画布坐标) */
    private int x;
    private int y;
    /** 节点参数 */
    private Map<String, Object> params;
    /** 输入连接(key:输入端口,value:源节点ID+输出端口) */
    private Map<String, String> inputs;
    /** 输出连接(key:输出端口,value:目标节点ID+输入端口) */
    private Map<String, String> outputs;
    /** 执行状态(pending/running/success/failed) */
    private String status = "pending";
    /** 执行结果 */
    private Object result;
}
3.2 流程定义(ComfyWorkflow.java)
package com.example.demo.model;

import lombok.Data;

import java.util.List;
import java.util.Map;

/**
 * ComfyUI 流程定义(最小功能)
 */
@Data
public class ComfyWorkflow {
    /** 流程ID */
    private String workflowId;
    /** 节点列表 */
    private List<ComfyNode> nodes;
    /** 节点执行顺序(按拖拽连线排序) */
    private List<String> nodeOrder;
    /** 全局参数 */
    private Map<String, Object> globalParams;
}
3.3 执行结果(WorkflowExecuteResult.java)
package com.example.demo.model;

import lombok.Data;

import java.util.Map;

/**
 * 流程执行结果
 */
@Data
public class WorkflowExecuteResult {
    /** 执行状态(success/failed) */
    private String status;
    /** 节点执行结果(key:节点ID,value:结果) */
    private Map<String, Object> nodeResults;
    /** 错误信息 */
    private String errorMsg;
}
4. 流程执行引擎(ComfyWorkflowEngine.java)
package com.example.demo.engine;

import com.example.demo.model.ComfyNode;
import com.example.demo.model.ComfyWorkflow;
import com.example.demo.model.WorkflowExecuteResult;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * ComfyUI 风格流程执行引擎(最小功能)
 */
@Component
public class ComfyWorkflowEngine {
    private final ChatClient chatClient;

    public ComfyWorkflowEngine(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    /**
     * 执行流程(按节点顺序执行,传递中间结果)
     */
    public WorkflowExecuteResult execute(ComfyWorkflow workflow) {
        WorkflowExecuteResult result = new WorkflowExecuteResult();
        Map<String, Object> nodeResults = new HashMap<>();
        result.setNodeResults(nodeResults);

        try {
            List<String> nodeOrder = workflow.getNodeOrder();
            if (nodeOrder == null || nodeOrder.isEmpty()) {
                throw new RuntimeException("流程无执行节点");
            }

            // 按顺序执行每个节点
            for (String nodeId : nodeOrder) {
                ComfyNode node = findNodeById(workflow.getNodes(), nodeId);
                if (node == null) {
                    throw new RuntimeException("节点不存在:" + nodeId);
                }

                node.setStatus("running");
                Object nodeResult = executeNode(node, nodeResults);
                node.setResult(nodeResult);
                node.setStatus("success");
                nodeResults.put(nodeId, nodeResult);
            }

            result.setStatus("success");
        } catch (Exception e) {
            result.setStatus("failed");
            result.setErrorMsg(e.getMessage());
        }

        return result;
    }

    /**
     * 执行单个节点
     */
    private Object executeNode(ComfyNode node, Map<String, Object> prevResults) {
        switch (node.getNodeType()) {
            case "prompt":
                // Prompt节点:返回拼接后的提示词(支持引用前序节点结果)
                String promptTemplate = (String) node.getParams().get("promptTemplate");
                String finalPrompt = replaceNodeReferences(promptTemplate, prevResults);
                return finalPrompt;
            case "llm":
                // LLM节点:调用大模型(输入为前序Prompt节点结果)
                String inputNodeId = getInputNodeId(node.getInputs().get("prompt"));
                String prompt = (String) prevResults.get(inputNodeId);
                return chatClient.call(new Prompt(prompt)).getResult().getOutput().getContent();
            case "in_output":
                // 输出节点:返回最终结果
                String outputInputNodeId = getInputNodeId(node.getInputs().get("content"));
                return prevResults.get(outputInputNodeId);
            default:
                throw new RuntimeException("不支持的节点类型:" + node.getNodeType());
        }
    }

    /**
     * 查找节点
     */
    private ComfyNode findNodeById(List<ComfyNode> nodes, String nodeId) {
        return nodes.stream().filter(n -> nodeId.equals(n.getNodeId())).findFirst().orElse(null);
    }

    /**
     * 替换提示词中的节点引用(如 ${node1})
     */
    private String replaceNodeReferences(String template, Map<String, Object> nodeResults) {
        if (template == null || !template.contains("${")) {
            return template;
        }
        for (Map.Entry<String, Object> entry : nodeResults.entrySet()) {
            template = template.replace("${" + entry.getKey() + "}", entry.getValue().toString());
        }
        return template;
    }

    /**
     * 解析输入连接中的源节点ID(格式:node1:output)
     */
    private String getInputNodeId(String inputStr) {
        if (inputStr == null) {
            return null;
        }
        return inputStr.split(":")[0];
    }
}
5. 控制器(ComfyUIController.java)
package com.example.demo.controller;

import com.example.demo.engine.ComfyWorkflowEngine;
import com.example.demo.model.ComfyWorkflow;
import com.example.demo.model.WorkflowExecuteResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

/**
 * ComfyUI 风格控制器(最小功能)
 */
@Controller
@RequestMapping("/comfyui")
public class ComfyUIController {
    @Autowired
    private ComfyWorkflowEngine workflowEngine;

    /**
     * 首页(画布+节点编排)
     */
    @GetMapping("/index")
    public String index() {
        return "index";
    }

    /**
     * 执行流程
     */
    @PostMapping("/execute")
    @ResponseBody
    public WorkflowExecuteResult executeWorkflow(@RequestBody ComfyWorkflow workflow) {
        return workflowEngine.execute(workflow);
    }

    /**
     * 获取示例流程(快速体验)
     */
    @GetMapping("/example-workflow")
    @ResponseBody
    public ComfyWorkflow getExampleWorkflow() {
        ComfyWorkflow workflow = new ComfyWorkflow();
        workflow.setWorkflowId("comfy_demo_001");
        
        // 1. 创建Prompt节点
        ComfyNode promptNode = new ComfyNode();
        promptNode.setNodeId("node1");
        promptNode.setNodeType("prompt");
        promptNode.setNodeName("Prompt节点");
        promptNode.setX(100);
        promptNode.setY(100);
        promptNode.getParams().put("promptTemplate", "写一段关于${topic}的温馨短句,不超过50字");
        promptNode.getParams().put("topic", "春天");
        
        // 2. 创建LLM节点
        ComfyNode llmNode = new ComfyNode();
        llmNode.setNodeId("node2");
        llmNode.setNodeType("llm");
        llmNode.setNodeName("LLM调用节点");
        llmNode.setX(300);
        llmNode.setY(100);
        llmNode.getInputs().put("prompt", "node1:output");
        
        // 3. 创建输出节点
        ComfyNode outputNode = new ComfyNode();
        outputNode.setNodeId("node3");
        outputNode.setNodeType("in_output");
        outputNode.setNodeName("输出节点");
        outputNode.setX(500);
        outputNode.setY(100);
        outputNode.getInputs().put("content", "node2:output");
        
        // 4. 节点列表和执行顺序
        workflow.getNodes().add(promptNode);
        workflow.getNodes().add(llmNode);
        workflow.getNodes().add(outputNode);
        workflow.getNodeOrder().add("node1");
        workflow.getNodeOrder().add("node2");
        workflow.getNodeOrder().add("node3");
        
        // 全局参数
        workflow.getGlobalParams().put("topic", "春天");
        
        return workflow;
    }
}
6. 启动类(ComfyUIDemoApplication.java)
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ComfyUIDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(ComfyUIDemoApplication.class, args);
        System.out.println("===== Spring AI 模拟 ComfyUI Demo 启动成功 =====");
        System.out.println("访问地址:http://localhost:8080/comfyui/index");
    }
}
7. 前端页面(核心:EasyUI + 拖拽画布)
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Spring AI 模拟 ComfyUI 大模型工作流</title>
    <!-- EasyUI 核心 -->
    <link rel="stylesheet" type="text/css" href="https://www.jeasyui.com/easyui/themes/default/easyui.css">
    <link rel="stylesheet" type="text/css" href="https://www.jeasyui.com/easyui/themes/icon.css">
    <script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
    <script type="text/javascript" src="https://www.jeasyui.com/easyui/jquery.easyui.min.js"></script>
    
    <style>
        /* 核心样式(模拟ComfyUI画布) */
        body {
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
        }
        .comfy-container {
            display: flex;
            height: 100vh;
        }
        /* 节点库面板 */
        .node-library {
            width: 200px;
            background: #f5f5f5;
            border-right: 1px solid #ddd;
            padding: 10px;
        }
        .node-item {
            padding: 10px;
            background: #fff;
            border: 1px solid #ccc;
            border-radius: 4px;
            margin-bottom: 10px;
            cursor: move;
            text-align: center;
            font-size: 14px;
        }
        .node-item:hover {
            background: #e8f4f8;
        }
        /* 画布区域 */
        .canvas-area {
            flex: 1;
            position: relative;
            background: #fafafa;
            overflow: auto;
        }
        /* 画布中的节点样式 */
        .canvas-node {
            position: absolute;
            width: 180px;
            background: #fff;
            border: 1px solid #666;
            border-radius: 6px;
            padding: 10px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            cursor: move;
        }
        .canvas-node .node-header {
            font-weight: bold;
            padding-bottom: 5px;
            border-bottom: 1px solid #eee;
            margin-bottom: 5px;
        }
        .canvas-node .node-ports {
            display: flex;
            justify-content: space-between;
            margin-top: 8px;
        }
        .port {
            width: 10px;
            height: 10px;
            background: #4285f4;
            border-radius: 50%;
            cursor: pointer;
        }
        /* 控制栏 */
        .control-bar {
            position: fixed;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            background: #fff;
            padding: 10px 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        /* 结果面板 */
        .result-panel {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 300px;
            background: #fff;
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 10px;
            max-height: 400px;
            overflow: auto;
            display: none;
        }
        .result-panel h4 {
            margin: 0 0 10px 0;
            padding-bottom: 5px;
            border-bottom: 1px solid #eee;
        }
        .node-result {
            margin-bottom: 8px;
            padding: 5px;
            background: #f5f5f5;
            border-radius: 4px;
        }
    </style>
</head>
<body>
    <div class="comfy-container">
        <!-- 节点库 -->
        <div class="node-library">
            <h4>节点库</h4>
            <div class="node-item" draggable="true" data-node-type="prompt">Prompt节点</div>
            <div class="node-item" draggable="true" data-node-type="llm">LLM调用节点</div>
            <div class="node-item" draggable="true" data-node-type="in_output">输出节点</div>
        </div>

        <!-- 画布区域 -->
        <div class="canvas-area" id="canvas">
            <div style="position: absolute; top: 10px; left: 10px; color: #666;">
                🖱️ 拖拽左侧节点到画布 | 双击节点配置参数 | 点击执行流程
            </div>
        </div>
    </div>

    <!-- 控制栏 -->
    <div class="control-bar">
        <a href="javascript:void(0)" class="easyui-linkbutton" onclick="loadExample()" data-options="iconCls:'icon-load'">加载示例流程</a>
        <a href="javascript:void(0)" class="easyui-linkbutton" onclick="executeWorkflow()" data-options="iconCls:'icon-play'">执行流程</a>
        <a href="javascript:void(0)" class="easyui-linkbutton" onclick="clearCanvas()" data-options="iconCls:'icon-clear'">清空画布</a>
    </div>

    <!-- 结果面板 -->
    <div class="result-panel" id="resultPanel">
        <h4>流程执行结果</h4>
        <div id="resultContent"></div>
    </div>

    <!-- 节点配置弹窗 -->
    <div id="nodeConfigDialog" class="easyui-dialog" title="节点参数配置" style="width: 400px; height: 300px;"
         data-options="closed:true, modal:true, buttons:[
             {text:'保存', iconCls:'icon-save', handler:function(){saveNodeConfig();}},
             {text:'取消', iconCls:'icon-cancel', handler:function(){$('#nodeConfigDialog').dialog('close');}}
         ]">
        <form id="nodeConfigForm" style="padding: 20px;">
            <div id="configFormContent"></div>
        </form>
    </div>

    <script type="text/javascript">
        // 全局变量
        let currentNodeId = null;
        let workflow = {
            workflowId: "demo_001",
            nodes: [],
            nodeOrder: [],
            globalParams: {}
        };

        // 初始化拖拽
        $(function() {
            // 节点库拖拽
            $('.node-item').on('dragstart', function(e) {
                e.originalEvent.dataTransfer.setData('nodeType', $(this).data('node-type'));
                e.originalEvent.dataTransfer.setData('nodeName', $(this).text());
            });

            // 画布拖放
            $('#canvas').on('dragover', function(e) {
                e.preventDefault();
            });

            $('#canvas').on('drop', function(e) {
                e.preventDefault();
                const nodeType = e.originalEvent.dataTransfer.getData('nodeType');
                const nodeName = e.originalEvent.dataTransfer.getData('nodeName');
                const x = e.originalEvent.offsetX;
                const y = e.originalEvent.offsetY;
                
                // 创建画布节点
                createCanvasNode(nodeType, nodeName, x, y);
            });

            // 画布节点拖拽
            $('#canvas').on('mousedown', '.canvas-node', function(e) {
                const node = $(this);
                const startX = e.pageX - node.offset().left;
                const startY = e.pageY - node.offset().top;
                
                $(document).on('mousemove', function(e) {
                    node.css({
                        left: e.pageX - startX + 'px',
                        top: e.pageY - startY + 'px'
                    });
                });
                
                $(document).on('mouseup', function() {
                    $(document).off('mousemove');
                    $(document).off('mouseup');
                    // 更新节点坐标
                    updateNodePosition(node.attr('data-node-id'), node.position().left, node.position().top);
                });
            });

            // 双击节点配置参数
            $('#canvas').on('dblclick', '.canvas-node', function() {
                currentNodeId = $(this).attr('data-node-id');
                showNodeConfigDialog(currentNodeId);
            });
        });

        // 创建画布节点
        function createCanvasNode(nodeType, nodeName, x, y) {
            const nodeId = 'node_' + Date.now();
            const nodeHtml = `
                <div class="canvas-node" data-node-id="${nodeId}" data-node-type="${nodeType}" style="left: ${x}px; top: ${y}px;">
                    <div class="node-header">${nodeName}</div>
                    <div class="node-type">类型:${nodeType}</div>
                    <div class="node-ports">
                        <div class="port input-port" title="输入端口"></div>
                        <div class="port output-port" title="输出端口"></div>
                    </div>
                </div>
            `;
            $('#canvas').append(nodeHtml);

            // 添加到流程定义
            const node = {
                nodeId: nodeId,
                nodeType: nodeType,
                nodeName: nodeName,
                x: x,
                y: y,
                params: {},
                inputs: {},
                outputs: {},
                status: "pending"
            };
            workflow.nodes.push(node);
            workflow.nodeOrder.push(nodeId);

            // 默认参数
            if (nodeType === 'prompt') {
                node.params.promptTemplate = "请输入提示词模板(支持${节点ID}引用)";
            }
        }

        // 更新节点坐标
        function updateNodePosition(nodeId, x, y) {
            const node = workflow.nodes.find(n => n.nodeId === nodeId);
            if (node) {
                node.x = x;
                node.y = y;
            }
        }

        // 显示节点配置弹窗
        function showNodeConfigDialog(nodeId) {
            const node = workflow.nodes.find(n => n.nodeId === nodeId);
            if (!node) return;

            let configHtml = '';
            switch (node.nodeType) {
                case 'prompt':
                    configHtml = `
                        <div style="margin-bottom: 15px;">
                            <label>提示词模板:</label>
                            <textarea name="promptTemplate" style="width: 100%; height: 80px;">${node.params.promptTemplate || ''}</textarea>
                        </div>
                        <div style="margin-bottom: 15px;">
                            <label>测试变量(topic):</label>
                            <input name="topic" type="text" style="width: 100%;" value="${node.params.topic || ''}">
                        </div>
                    `;
                    break;
                case 'llm':
                    configHtml = `
                        <div style="margin-bottom: 15px;">
                            <label>输入节点(Prompt节点ID):</label>
                            <input name="inputNode" type="text" style="width: 100%;" value="${node.inputs.prompt ? node.inputs.prompt.split(':')[0] : ''}">
                        </div>
                        <div style="margin-bottom: 15px;">
                            <label>模型:</label>
                            <select name="model" style="width: 100%;">
                                <option value="gpt-3.5-turbo" selected>gpt-3.5-turbo</option>
                                <option value="gpt-4">gpt-4</option>
                            </select>
                        </div>
                    `;
                    break;
                case 'in_output':
                    configHtml = `
                        <div style="margin-bottom: 15px;">
                            <label>输入节点(LLM节点ID):</label>
                            <input name="inputNode" type="text" style="width: 100%;" value="${node.inputs.content ? node.inputs.content.split(':')[0] : ''}">
                        </div>
                    `;
                    break;
            }

            $('#configFormContent').html(configHtml);
            $('#nodeConfigDialog').dialog('open');
        }

        // 保存节点配置
        function saveNodeConfig() {
            const node = workflow.nodes.find(n => n.nodeId === currentNodeId);
            if (!node) return;

            const formData = $('#nodeConfigForm').serializeArray();
            formData.forEach(item => {
                if (node.nodeType === 'prompt') {
                    node.params[item.name] = item.value;
                } else if (node.nodeType === 'llm') {
                    if (item.name === 'inputNode') {
                        node.inputs.prompt = item.value + ":output";
                    } else {
                        node.params[item.name] = item.value;
                    }
                } else if (node.nodeType === 'in_output') {
                    if (item.name === 'inputNode') {
                        node.inputs.content = item.value + ":output";
                    }
                }
            });

            $('#nodeConfigDialog').dialog('close');
            $.messager.alert('提示', '节点配置已保存!', 'info');
        }

        // 加载示例流程
        function loadExample() {
            $.ajax({
                url: '/comfyui/example-workflow',
                type: 'GET',
                success: function(data) {
                    workflow = data;
                    clearCanvas(true);
                    
                    // 渲染示例节点到画布
                    workflow.nodes.forEach(node => {
                        const nodeHtml = `
                            <div class="canvas-node" data-node-id="${node.nodeId}" data-node-type="${node.nodeType}" style="left: ${node.x}px; top: ${node.y}px;">
                                <div class="node-header">${node.nodeName}</div>
                                <div class="node-type">类型:${node.nodeType}</div>
                                <div class="node-ports">
                                    <div class="port input-port" title="输入端口"></div>
                                    <div class="port output-port" title="输出端口"></div>
                                </div>
                            </div>
                        `;
                        $('#canvas').append(nodeHtml);
                    });

                    $.messager.alert('提示', '示例流程已加载!', 'info');
                }
            });
        }

        // 执行流程
        function executeWorkflow() {
            if (workflow.nodes.length === 0) {
                $.messager.alert('提示', '画布中无节点,请先添加节点!', 'warning');
                return;
            }

            $.ajax({
                url: '/comfyui/execute',
                type: 'POST',
                contentType: 'application/json',
                data: JSON.stringify(workflow),
                beforeSend: function() {
                    $.messager.progress({title:'提示', msg:'流程执行中...'});
                },
                success: function(result) {
                    $.messager.progress('close');
                    if (result.status === 'success') {
                        // 显示结果面板
                        let resultHtml = '';
                        for (const [nodeId, nodeResult] of Object.entries(result.nodeResults)) {
                            const node = workflow.nodes.find(n => n.nodeId === nodeId);
                            resultHtml += `
                                <div class="node-result">
                                    <strong>${node.nodeName}(${nodeId}):</strong>
                                    <div>${JSON.stringify(nodeResult, null, 2)}</div>
                                </div>
                            `;
                        }
                        $('#resultContent').html(resultHtml);
                        $('#resultPanel').show();
                        $.messager.alert('成功', '流程执行完成!', 'info');
                    } else {
                        $.messager.alert('失败', result.errorMsg, 'error');
                    }
                },
                error: function() {
                    $.messager.progress('close');
                    $.messager.alert('错误', '流程执行请求失败!', 'error');
                }
            });
        }

        // 清空画布
        function clearCanvas(skipConfirm) {
            if (!skipConfirm && !confirm('确定清空画布吗?')) {
                return;
            }
            $('#canvas').find('.canvas-node').remove();
            workflow = {
                workflowId: "demo_001",
                nodes: [],
                nodeOrder: [],
                globalParams: {}
            };
            $('#resultPanel').hide();
        }
    </script>
</body>
</html>
三、Demo 运行与核心演示步骤
1. 环境准备
  1. 替换 ​​application.yml​​ 中的 ​​spring.ai.openai.api-key​​ 为你的 OpenAI API Key;
  2. JDK 8+/11,无需数据库(纯内存执行)。
2. 核心演示流程
步骤1:加载示例流程(快速体验)
  1. 访问 ​​http://localhost:8080/comfyui/index​​;
  2. 点击「加载示例流程」→ 画布中出现 3 个节点:Prompt 节点(100,100)、LLM 调用节点(300,100)、输出节点(500,100);
  3. 双击「Prompt 节点」→ 可看到预配置的提示词模板:​​写一段关于${topic}的温馨短句,不超过50字​​,topic 为「春天」。
步骤2:执行流程
  1. 点击「执行流程」→ 等待几秒后弹出“流程执行完成”;
  2. 右侧结果面板展示每个节点的执行结果:
  • Prompt 节点:拼接后的提示词 ​​写一段关于春天的温馨短句,不超过50字​​;
  • LLM 调用节点:AI 生成的春天短句(如「春风轻拂,繁花盛放,愿你的日子如春日般温柔绚烂✨」);
  • 输出节点:最终的 AI 生成结果。
步骤3:自定义节点编排(最小功能体验)
  1. 点击「清空画布」;
  2. 拖拽左侧「Prompt 节点」到画布 → 双击配置提示词模板(如 ​​写一个${product}的广告语​​,product 填「咖啡」);
  3. 拖拽「LLM 调用节点」到画布 → 双击配置输入节点为 Prompt 节点的 ID(如 node_1712345678901);
  4. 拖拽「输出节点」到画布 → 双击配置输入节点为 LLM 节点的 ID;
  5. 点击「执行流程」→ 查看自定义流程的执行结果。
四、核心设计(类 ComfyUI 最小功能)
flowchart TD
    A[拖拽节点到画布] --> B[配置节点参数/连接关系]
    B --> C[流程引擎按顺序执行节点]
    C --> D[Prompt节点:拼接提示词]
    D --> E[LLM节点:调用Spring AI生成结果]
    E --> F[输出节点:返回最终结果]
    F --> G[可视化展示执行结果]
五、总结
  1. 核心实现:该 Demo 是 Spring AI + EasyUI 模拟 ComfyUI 的最小功能版本,聚焦 ComfyUI 的核心体验——节点拖拽编排 + 参数配置 + 流程链路执行 + 结果可视化
  2. 关键技术点
  • HTML5 拖拽 API 实现节点画布拖拽,模拟 ComfyUI 的可视化编排;
  • Spring AI 封装为 LLM 节点,支持节点间结果传递(引用前序节点输出);
  • 极简的流程执行引擎,按节点顺序执行并返回每个节点的结果;
  1. 扩展方向(贴近完整 ComfyUI)
  • 增加节点连线可视化(SVG/Canvas 绘制连线);
  • 支持更多节点类型(图片生成、文本拆分、条件分支);
  • 节点参数实时校验、流程断点调试;
  • 集成多模型(Stable Diffusion 图片生成、讯飞星火等)。

这个 Demo 完美还原了 ComfyUI “节点化编排 AI 工作流”的核心思路,是 Spring AI 实现大模型可视化工作流的最小可运行版本,你可基于此扩展更多 ComfyUI 特性。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐