Dify可视化编排:技术架构与实战指南

Dify 前端可视化编排深度解析:技术架构 × 源码剖析 × 嵌入实战

作者 :前端架构师 | 发布时间 :2026-04-24 | 阅读时长 :约 25 分钟
标签#Dify #ReactFlow #可视化编排 #前端架构 #二次开发 #Zustand #TypeScript


目录


一、可视化编排模块定位与技术选型

Dify 的 Workflow Builder 是整个平台最复杂的前端模块------它是一个生产级的 DAG 流程图编辑器,需要同时满足:

需求 具体要求
可视化编辑 拖拽创建节点、连线、选择、缩放、小地图
节点配置 每种节点类型有独立配置面板(右侧 Panel)
变量系统 跨节点变量引用,类型安全校验
实时执行 运行时节点状态流式更新(SSE 推送)
历史记录 撤销/重做(Undo/Redo)
DSL 导入导出 YAML 格式的工作流序列化/反序列化

为什么选 ReactFlow(@xyflow/react)?

复制代码
候选方案对比:
  X6(AntV)    → 重,中文生态好,但 React 集成不够原生
  LogicFlow     → 业务流程图,节点自定义能力弱
  Mermaid       → 纯展示,不支持交互编辑
  D3.js         → 底层灵活但需要大量封装
✅ ReactFlow    → React 原生、节点完全自定义、性能优秀、社区活跃

二、前端技术栈全景

复制代码
web/
├── 框架层      Next.js 14 (App Router + SSR)
├── UI 框架     React 18 + TypeScript 5
├── 画布引擎    @xyflow/react(ReactFlow v12)
├── 状态管理    Zustand(多 Store 分层)
├── 不可变更新  immer(produce)
├── 样式方案    TailwindCSS + CSS Modules
├── 数据获取    SWR(缓存 + 重验证)
├── 国际化      i18next
└── 图标        Remixicon / 自定义 SVG

三、源码目录结构详解

复制代码
web/app/components/workflow/
│
├── index.tsx                    ← 主入口,ReactFlow 画布初始化
├── context.tsx                  ← React Context,提供全局配置
├── constants.ts                 ← 常量(节点尺寸、颜色等)
├── types.ts                     ← 全局 TypeScript 类型定义
├── features.tsx                 ← ReactFlow Features 配置
│
├── nodes/                       ← 🔑 所有节点组件
│   ├── _base/                   ← 节点基础组件(Handle、Header、状态指示器)
│   │   ├── components/          ← 共享子组件(Field、VarReferencePicker、OutputVars...)
│   │   ├── hooks/               ← 节点通用 Hooks
│   │   └── node.tsx             ← 节点外壳(统一样式、选中态)
│   │
│   ├── llm/                     ← LLM 节点
│   │   ├── index.tsx            ← 画布节点组件
│   │   ├── panel.tsx            ← 右侧配置面板
│   │   ├── use-config.ts        ← 面板状态管理 Hook
│   │   ├── types.ts             ← 类型定义
│   │   └── default.ts           ← 默认值 & 校验逻辑
│   │
│   ├── knowledge-retrieval/     ← 知识库检索节点
│   ├── if-else/                 ← 条件分支节点
│   ├── http-request/            ← HTTP 请求节点
│   ├── code/                    ← 代码执行节点
│   ├── start/                   ← 开始节点
│   ├── end/                     ← 结束节点
│   ├── question-classifier/     ← 问题分类节点
│   ├── template-transform/      ← 模板转换节点
│   ├── tool/                    ← 工具节点
│   ├── iteration/               ← 迭代节点
│   ├── variable-assigner/       ← 变量赋值节点
│   └── agent/                   ← Agent 节点
│
├── store/                       ← 🔑 Zustand 状态管理
│   ├── index.ts                 ← Store 聚合导出
│   ├── workflow-store.ts        ← 主工作流状态(nodes/edges/运行态)
│   └── workflow-history-store.tsx ← Undo/Redo 历史记录
│
├── hooks/                       ← 🔑 核心业务 Hooks
│   ├── use-workflow.ts          ← 工作流全局管理(最重要!)
│   ├── use-nodes-interactions.ts ← 节点拖拽/选择交互
│   ├── use-edges-interactions.ts ← 连线交互
│   ├── use-workflow-run.ts      ← 工作流执行
│   └── use-nodes-sync-draft.ts  ← 草稿自动同步
│
├── panel/                       ← 画布面板(工具栏、小地图、操作按钮)
├── run/                         ← 执行结果展示面板
├── operator/                    ← 画布操作器(缩放/全屏等)
├── block-selector/              ← 节点选择器(添加节点弹窗)
├── custom-edge.tsx              ← 自定义连线组件
└── custom-connection-line.tsx   ← 实时拖拽连线样式

四、核心架构:ReactFlow 画布层

4.1 画布初始化(index.tsx 核心逻辑)

tsx 复制代码
// web/app/components/workflow/index.tsx(简化版)
import { ReactFlow, Background, Controls, MiniMap } from '@xyflow/react'
import { useWorkflowStore } from './store'
import { nodeTypes } from './nodes'          // 节点类型注册表
import { edgeTypes } from './custom-edge'    // 自定义边类型
import CustomConnectionLine from './custom-connection-line'

const WorkflowCanvas: FC = () => {
  const {
    nodes,
    edges,
    onNodesChange,
    onEdgesChange,
    onConnect,
  } = useWorkflowStore()

  const {
    handleNodeClick,
    handlePaneClick,
    handleNodeDragStop,
  } = useNodesInteractions()

  return (
    <ReactFlowProvider>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        nodeTypes={nodeTypes}                    // 注册所有自定义节点
        edgeTypes={edgeTypes}                    // 注册自定义边
        connectionLineComponent={CustomConnectionLine}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
        onNodeClick={handleNodeClick}
        onPaneClick={handlePaneClick}
        onNodeDragStop={handleNodeDragStop}
        fitView
        minZoom={0.25}
        maxZoom={2}
        snapToGrid
        snapGrid={[8, 8]}
      >
        <Background />
        <Controls />
        <MiniMap />
      </ReactFlow>
    </ReactFlowProvider>
  )
}

4.2 节点类型注册表

typescript 复制代码
// web/app/components/workflow/nodes/index.ts
import { BlockEnum } from '../types'
import StartNode from './start'
import EndNode from './end'
import LLMNode from './llm'
import KnowledgeRetrievalNode from './knowledge-retrieval'
import IfElseNode from './if-else'
import HttpRequestNode from './http-request'
import CodeNode from './code'
import ToolNode from './tool'
import IterationNode from './iteration'
import AgentNode from './agent'

export const nodeTypes: Record<string, ComponentType<NodeProps>> = {
  [BlockEnum.Start]: StartNode,
  [BlockEnum.End]: EndNode,
  [BlockEnum.LLM]: LLMNode,
  [BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalNode,
  [BlockEnum.IfElse]: IfElseNode,
  [BlockEnum.HttpRequest]: HttpRequestNode,
  [BlockEnum.Code]: CodeNode,
  [BlockEnum.Tool]: ToolNode,
  [BlockEnum.Iteration]: IterationNode,
  [BlockEnum.Agent]: AgentNode,
}

4.3 自定义连线(custom-edge.tsx)

tsx 复制代码
import { EdgeLabelRenderer, getBezierPath, EdgeProps } from '@xyflow/react'
import { CommonEdgeType } from './types'

const CustomEdge: FC<EdgeProps<CommonEdgeType>> = ({
  id, sourceX, sourceY, targetX, targetY,
  sourcePosition, targetPosition, data, selected,
}) => {
  const [edgePath, labelX, labelY] = getBezierPath({
    sourceX, sourceY, sourcePosition,
    targetX, targetY, targetPosition,
  })

  const isRunning = data?._sourceRunningStatus === NodeRunningStatus.Running

  return (
    <>
      <path
        id={id}
        className={cn(
          'react-flow__edge-path',
          selected && 'stroke-primary-600',
          isRunning && 'stroke-blue-500 stroke-2 animate-pulse',
        )}
        d={edgePath}
      />
      {/* 运行中的动态小圆点 */}
      {isRunning && (
        <circle r="4" fill="#3b82f6">
          <animateMotion dur="1.5s" repeatCount="indefinite" path={edgePath} />
        </circle>
      )}
    </>
  )
}

五、节点系统深度剖析

5.1 节点枚举(BlockEnum)

typescript 复制代码
// web/app/components/workflow/types.ts
export enum BlockEnum {
  Start = 'start',
  End = 'end',
  Answer = 'answer',
  LLM = 'llm',
  KnowledgeRetrieval = 'knowledge-retrieval',
  QuestionClassifier = 'question-classifier',
  IfElse = 'if-else',
  Code = 'code',
  TemplateTransform = 'template-transform',
  HttpRequest = 'http-request',
  VariableAssigner = 'variable-assigner',
  VariableAggregator = 'variable-aggregator',
  Tool = 'tool',
  ParameterExtractor = 'parameter-extractor',
  Iteration = 'iteration',
  IterationStart = 'iteration-start',
  Agent = 'agent',
  Loop = 'loop',
  LoopStart = 'loop-start',
}

5.2 通用节点数据类型(CommonNodeType)

typescript 复制代码
export type CommonNodeType<T = {}> = T & {
  // ── 连接状态 ──
  _connectedSourceHandleIds?: string[]
  _connectedTargetHandleIds?: string[]
  _targetBranches?: Branch[]

  // ── 运行态(前端临时状态,不持久化)──
  _runningStatus?: NodeRunningStatus        // 'running' | 'succeeded' | 'failed'
  _singleRunningStatus?: NodeRunningStatus
  _waitingRun?: boolean
  _retryIndex?: number

  // ── 迭代/循环上下文 ──
  isInIteration?: boolean
  isInLoop?: boolean
  iteration_id?: string
  loop_id?: string

  // ── UI 辅助状态 ──
  selected?: boolean
  _isCandidate?: boolean                    // 候选连接高亮
  _inParallelHovering?: boolean

  // ── 持久化配置 ──
  title: string
  desc: string
  type: BlockEnum
  width?: number
  height?: number
  error_strategy?: ErrorHandleTypeEnum      // 错误处理策略
  retry_config?: WorkflowRetryConfig        // 重试配置
}

5.3 节点运行状态枚举

typescript 复制代码
export enum NodeRunningStatus {
  NotStart = 'not-start',
  Waiting = 'waiting',
  Running = 'running',
  Succeeded = 'succeeded',
  Failed = 'failed',
  Exception = 'exception',
  Retry = 'retry',
}

5.4 节点外壳组件(_base/node.tsx)

所有节点都共享同一个"外壳"组件,实现统一的选中态、状态指示器、标题栏:

tsx 复制代码
// web/app/components/workflow/nodes/_base/node.tsx
const BaseNode: FC<NodeProps<CommonNodeType>> = ({
  id, data, selected, children,
}) => {
  const { runningStatus, selected: isSelected } = data

  return (
    <div
      className={cn(
        'workflow-node rounded-xl border-2 bg-white shadow-md',
        'transition-all duration-200',
        isSelected && 'border-primary-600 shadow-lg',
        runningStatus === NodeRunningStatus.Running && 'border-blue-400',
        runningStatus === NodeRunningStatus.Succeeded && 'border-green-500',
        runningStatus === NodeRunningStatus.Failed && 'border-red-500',
      )}
    >
      {/* 节点头部 */}
      <NodeHeader id={id} data={data} />

      {/* 节点内容(各节点自定义) */}
      <div className="p-3">{children}</div>

      {/* 运行状态角标 */}
      {runningStatus && (
        <StatusBadge status={runningStatus} retryIndex={data._retryIndex} />
      )}
    </div>
  )
}

六、状态管理:Zustand Store 分层设计

6.1 主工作流 Store

typescript 复制代码
// web/app/components/workflow/store/workflow-store.ts
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
import { produce } from 'immer'
import {
  applyNodeChanges, applyEdgeChanges,
  addEdge, type NodeChange, type EdgeChange, type Connection,
} from '@xyflow/react'

interface WorkflowState {
  // ── 画布数据 ──
  nodes: WorkflowNode[]
  edges: WorkflowEdge[]
  
  // ── 运行状态 ──
  isRunning: boolean
  runningNodeId: string | null
  
  // ── 草稿状态 ──
  isDraft: boolean
  workflowId: string
  
  // ── UI 状态 ──
  selectedNodeId: string | null
  hoveredNodeId: string | null
  
  // ── Actions ──
  setNodes: (nodes: WorkflowNode[]) => void
  setEdges: (edges: WorkflowEdge[]) => void
  onNodesChange: (changes: NodeChange[]) => void
  onEdgesChange: (changes: EdgeChange[]) => void
  onConnect: (connection: Connection) => void
  
  updateNodeData: (nodeId: string, data: Partial<CommonNodeType>) => void
  addNode: (node: WorkflowNode) => void
  deleteNode: (nodeId: string) => void
}

export const useWorkflowStore = create<WorkflowState>()(
  subscribeWithSelector((set, get) => ({
    nodes: [],
    edges: [],
    isRunning: false,
    runningNodeId: null,
    isDraft: false,
    workflowId: '',
    selectedNodeId: null,
    hoveredNodeId: null,

    setNodes: (nodes) => set({ nodes }),
    setEdges: (edges) => set({ edges }),

    onNodesChange: (changes) => set((state) => ({
      nodes: applyNodeChanges(changes, state.nodes),
    })),

    onEdgesChange: (changes) => set((state) => ({
      edges: applyEdgeChanges(changes, state.edges),
    })),

    onConnect: (connection) => set((state) => ({
      edges: addEdge(connection, state.edges),
    })),

    updateNodeData: (nodeId, data) => set((state) => ({
      nodes: state.nodes.map(node =>
        node.id === nodeId
          ? produce(node, (draft) => { Object.assign(draft.data, data) })
          : node
      ),
    })),

    addNode: (node) => set((state) => ({
      nodes: [...state.nodes, node],
    })),

    // 开始节点受保护,不可删除
    deleteNode: (nodeId) => set((state) => {
      const node = state.nodes.find(n => n.id === nodeId)
      if (node?.data.type === BlockEnum.Start) return {}
      return {
        nodes: state.nodes.filter(n => n.id !== nodeId),
        edges: state.edges.filter(
          e => e.source !== nodeId && e.target !== nodeId
        ),
      }
    }),
  }))
)

6.2 历史记录 Store(Undo/Redo)

typescript 复制代码
// web/app/components/workflow/store/workflow-history-store.tsx
interface HistorySnapshot {
  nodes: WorkflowNode[]
  edges: WorkflowEdge[]
  timestamp: number
}

interface HistoryState {
  past: HistorySnapshot[]
  future: HistorySnapshot[]
  maxHistorySize: number

  pushHistory: (snapshot: HistorySnapshot) => void
  undo: () => HistorySnapshot | null
  redo: () => HistorySnapshot | null
  canUndo: () => boolean
  canRedo: () => boolean
}

export const useHistoryStore = create<HistoryState>((set, get) => ({
  past: [],
  future: [],
  maxHistorySize: 50,

  pushHistory: (snapshot) => set((state) => {
    const past = [...state.past, snapshot].slice(-state.maxHistorySize)
    return { past, future: [] }  // 新操作清空 future
  }),

  undo: () => {
    const { past, future } = get()
    if (past.length === 0) return null
    const previous = past[past.length - 1]
    set({ past: past.slice(0, -1), future: [previous, ...future] })
    return previous
  },

  redo: () => {
    const { past, future } = get()
    if (future.length === 0) return null
    const next = future[0]
    set({ past: [...past, next], future: future.slice(1) })
    return next
  },

  canUndo: () => get().past.length > 0,
  canRedo: () => get().future.length > 0,
}))

七、变量引用系统:节点间数据流

这是 Dify 前端最核心的设计之一,理解它是二次开发的关键。

7.1 变量选择器格式

typescript 复制代码
// ValueSelector = [nodeId, ...keyPath]
// 例如:
['start_node_id', 'user_input']          // 开始节点的 user_input 字段
['llm_node_id', 'text']                  // LLM 节点的输出文本
['http_node_id', 'body', 'data', 'id']  // HTTP 节点嵌套字段
['sys', 'user_id']                       // 系统变量
['env', 'API_KEY']                       // 环境变量

7.2 变量类型系统

typescript 复制代码
export enum VarType {
  string = 'string',
  number = 'number',
  boolean = 'boolean',
  object = 'object',
  array = 'array',
  arrayString = 'array[string]',
  arrayNumber = 'array[number]',
  arrayObject = 'array[object]',
  arrayFile = 'array[file]',
  file = 'file',
  secret = 'secret',
  any = 'any',
}

7.3 VarReferencePicker 核心组件使用

tsx 复制代码
// 在自定义节点的 panel.tsx 中引用上游变量
import VarReferencePicker from '../_base/components/variable/var-reference-picker'

const MyNodePanel: FC<{ id: string; data: MyNodeType }> = ({ id, data }) => {
  const { inputs, setInputs } = useNodeCrud(id)

  return (
    <Field title="输入变量" required>
      <VarReferencePicker
        nodeId={id}
        value={inputs.input_variable}        // ValueSelector
        onChange={(selector) => {
          setInputs(produce(inputs, (draft) => {
            draft.input_variable = selector
          }))
        }}
        filterVar={(varPayload) =>            // 只允许选择字符串类型变量
          varPayload.type === VarType.string
        }
      />
    </Field>
  )
}

7.4 获取前置节点输出变量(use-workflow 核心能力)

typescript 复制代码
// 在节点内部获取所有可引用的上游变量
import { useAvailableVars } from '../hooks/use-workflow'

const { availableVars } = useAvailableVars(nodeId)
// availableVars: Array<{ nodeId, nodeType, vars: Variable[] }>
// 仅包含当前节点在 DAG 路径上的"祖先节点"输出变量

八、Panel 面板 + useConfig Hook 四件套

每个节点类型都遵循"四件套"约定,这是 Dify 前端最优雅的设计之一:

8.1 四件套结构

复制代码
nodes/my-custom-node/
├── index.tsx        ← 画布节点(显示在画布上的节点外观)
├── panel.tsx        ← 配置面板(右侧配置区 UI)
├── use-config.ts    ← 面板状态 Hook(状态 + 事件处理)
├── types.ts         ← TypeScript 类型
└── default.ts       ← 默认值 + 校验函数

8.2 types.ts

typescript 复制代码
// nodes/my-custom-node/types.ts
import { CommonNodeType } from '../../types'

export type MyCustomNodeType = CommonNodeType & {
  // 自定义配置字段
  api_url: string
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
  input_variable: ValueSelector
  output_schema: Record<string, VarType>
  timeout: number
}

8.3 default.ts

typescript 复制代码
// nodes/my-custom-node/default.ts
import { BlockEnum } from '../../types'
import type { MyCustomNodeType } from './types'

export const defaultConfig: Partial<MyCustomNodeType> = {
  type: BlockEnum.HttpRequest,
  title: '自定义节点',
  desc: '',
  api_url: '',
  method: 'POST',
  input_variable: [],
  timeout: 30000,
}

export function validateConfig(
  inputs: MyCustomNodeType,
  t: (key: string) => string
): string[] {
  const errors: string[] = []
  if (!inputs.api_url) errors.push(t('workflow.nodes.myNode.apiUrlRequired'))
  if (!inputs.input_variable?.length)
    errors.push(t('workflow.nodes.myNode.inputVarRequired'))
  return errors
}

8.4 use-config.ts

typescript 复制代码
// nodes/my-custom-node/use-config.ts
import { useCallback } from 'react'
import { produce } from 'immer'
import useNodeCrud from '../_base/hooks/use-node-crud'
import type { MyCustomNodeType } from './types'

const useConfig = (id: string, payload: MyCustomNodeType) => {
  const { inputs, setInputs } = useNodeCrud<MyCustomNodeType>(id, payload)

  const handleApiUrlChange = useCallback((url: string) => {
    const newInputs = produce(inputs, (draft) => {
      draft.api_url = url
    })
    setInputs(newInputs)
  }, [inputs, setInputs])

  const handleMethodChange = useCallback((method: MyCustomNodeType['method']) => {
    const newInputs = produce(inputs, (draft) => {
      draft.method = method
    })
    setInputs(newInputs)
  }, [inputs, setInputs])

  const handleInputVarChange = useCallback((selector: ValueSelector) => {
    const newInputs = produce(inputs, (draft) => {
      draft.input_variable = selector
    })
    setInputs(newInputs)
  }, [inputs, setInputs])

  return {
    inputs,
    handleApiUrlChange,
    handleMethodChange,
    handleInputVarChange,
  }
}

export default useConfig

8.5 panel.tsx

tsx 复制代码
// nodes/my-custom-node/panel.tsx
import { memo } from 'react'
import Field from '../_base/components/field'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
import { VarType } from '../../types'
import useConfig from './use-config'
import type { MyCustomNodeType } from './types'

const Panel: FC<{ id: string; data: MyCustomNodeType }> = memo(({ id, data }) => {
  const {
    inputs,
    handleApiUrlChange,
    handleMethodChange,
    handleInputVarChange,
  } = useConfig(id, data)

  return (
    <div className="space-y-4">
      {/* API URL 配置 */}
      <Field title="API 地址" required>
        <input
          value={inputs.api_url}
          onChange={e => handleApiUrlChange(e.target.value)}
          placeholder="https://api.example.com/endpoint"
          className="w-full px-3 py-2 rounded-lg border border-gray-200"
        />
      </Field>

      {/* 请求方法 */}
      <Field title="请求方法">
        <select
          value={inputs.method}
          onChange={e => handleMethodChange(e.target.value as any)}
          className="w-full px-3 py-2 rounded-lg border border-gray-200"
        >
          {['GET', 'POST', 'PUT', 'DELETE'].map(m => (
            <option key={m} value={m}>{m}</option>
          ))}
        </select>
      </Field>

      {/* 输入变量选择(引用上游节点输出) */}
      <Field title="输入变量" required>
        <VarReferencePicker
          nodeId={id}
          value={inputs.input_variable}
          onChange={handleInputVarChange}
          filterVar={(v) => v.type === VarType.string}
        />
      </Field>
    </div>
  )
})

export default Panel

8.6 index.tsx(画布节点外观)

tsx 复制代码
// nodes/my-custom-node/index.tsx
import { memo } from 'react'
import { Handle, Position, type NodeProps } from '@xyflow/react'
import NodeShell from '../_base/node'
import type { MyCustomNodeType } from './types'

const MyCustomNode: FC<NodeProps<MyCustomNodeType>> = memo(({ id, data, selected }) => {
  return (
    <NodeShell id={id} data={data} selected={selected}>
      {/* 输入 Handle */}
      <Handle type="target" position={Position.Left} id="target" />

      {/* 节点内容预览 */}
      <div className="text-sm text-gray-600 truncate max-w-[200px]">
        {data.api_url || '未配置 API 地址'}
      </div>

      {/* 输出 Handle */}
      <Handle type="source" position={Position.Right} id="source" />
    </NodeShell>
  )
})

export default MyCustomNode

九、工作流执行与实时反馈

9.1 执行流程

复制代码
用户点击「运行」
    ↓
useWorkflowRun.handleStartWorkflowRun()
    ↓
POST /api/workflows/run(带 inputs + mode)
    ↓
后端返回 task_id,建立 SSE 连接
    ↓
EventSource 监听 /api/workflows/run/streaming
    ↓
解析事件类型:
  node_started → 更新节点状态为 Running(高亮蓝色)
  node_finished → 更新节点状态为 Succeeded/Failed(绿/红)
  workflow_finished → 全部完成,渲染输出

9.2 SSE 事件处理

typescript 复制代码
// hooks/use-workflow-run.ts(简化)
const handleSSEMessage = useCallback((event: MessageEvent) => {
  const data = JSON.parse(event.data)

  switch (data.event) {
    case 'node_started':
      updateNodeData(data.data.node_id, {
        _runningStatus: NodeRunningStatus.Running,
      })
      break

    case 'node_finished':
      updateNodeData(data.data.node_id, {
        _runningStatus: data.data.status === 'succeeded'
          ? NodeRunningStatus.Succeeded
          : NodeRunningStatus.Failed,
        // 存储节点输出,供调试面板展示
        _nodeRunResult: data.data.outputs,
      })
      break

    case 'workflow_finished':
      setIsRunning(false)
      setWorkflowRunResult(data.data)
      break
  }
}, [updateNodeData])

十、嵌入方案一:官方 SDK / iframe(快速集成)

这是最快速、零维护成本的方案,适合需要在现有系统中嵌入 Dify 应用能力(聊天机器人、对话界面)。

10.1 方案对比

嵌入方式 开发成本 定制程度 维护成本 适用场景
iframe 极低(5分钟) 低(仅样式) 极低 快速上线、外部系统集成
JS SDK 低(1小时) 中(交互可控) 需要自定义触发时机和交互
API 直调 中(1-2天) 高(完全自定义UI) 自研UI组件,调用Dify后端
源码二开 高(1-2周+) 极高(一切可改) 深度定制,嵌入编排画布本身

10.2 iframe 嵌入

html 复制代码
<!-- 基础嵌入 -->
<iframe
  src="https://your-dify.com/chatbot/YOUR_APP_TOKEN"
  width="100%"
  height="600"
  style="border: none; border-radius: 12px;"
  allow="microphone">
</iframe>

<!-- 响应式全屏 -->
<div style="position:relative; width:100%; padding-bottom:56.25%;">
  <iframe
    src="https://your-dify.com/chatbot/YOUR_APP_TOKEN"
    style="position:absolute; top:0; left:0; width:100%; height:100%; border:none;"
  ></iframe>
</div>

10.3 JS SDK 嵌入(聊天气泡 + 预填参数)

html 复制代码
<script>
window.difyChatbotConfig = {
  token: 'YOUR_APP_TOKEN',
  baseUrl: 'https://your-dify.com',      // 自托管地址

  // 位置与样式
  containerProps: {
    style: {
      right: '24px',
      bottom: '24px',
    },
    className: 'my-custom-chat-btn',
  },

  // 可拖拽
  draggable: true,
  dragAxis: 'both',

  // 预填充用户上下文(对应应用中定义的变量)
  inputs: {
    user_name: window.__currentUser?.name || '访客',
    user_role: window.__currentUser?.role || 'guest',
    page_context: window.location.pathname,
  },

  // 系统变量追踪
  systemVariables: {
    user_id: window.__currentUser?.id,
  },
}
</script>
<script src="https://your-dify.com/embed.js" async></script>

10.4 API 直调(完全自研 UI)

typescript 复制代码
// 调用 Dify Chat API,自定义 UI
class DifyApiClient {
  constructor(
    private baseUrl: string,
    private apiKey: string,
  ) {}

  async *streamChat(params: {
    query: string
    conversationId?: string
    inputs?: Record<string, string>
    userId: string
  }): AsyncGenerator<ChatChunk> {
    const response = await fetch(`${this.baseUrl}/v1/chat-messages`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: params.query,
        conversation_id: params.conversationId,
        inputs: params.inputs ?? {},
        user: params.userId,
        response_mode: 'streaming',
      }),
    })

    const reader = response.body!.getReader()
    const decoder = new TextDecoder()

    while (true) {
      const { done, value } = await reader.read()
      if (done) break

      const chunk = decoder.decode(value)
      const lines = chunk.split('\n').filter(l => l.startsWith('data: '))

      for (const line of lines) {
        const data = JSON.parse(line.slice(6))
        if (data.event === 'message') {
          yield { text: data.answer, conversationId: data.conversation_id }
        }
      }
    }
  }
}

// React 组件中使用
const ChatWidget: FC = () => {
  const [messages, setMessages] = useState<Message[]>([])
  const client = new DifyApiClient('/dify-api', 'app-xxxx')

  const handleSend = async (query: string) => {
    for await (const chunk of client.streamChat({ query, userId: 'user-1' })) {
      // 流式追加消息
      setMessages(prev => appendToLastMessage(prev, chunk.text))
    }
  }

  return <YourCustomChatUI messages={messages} onSend={handleSend} />
}

十一、嵌入方案二:源码级二次开发(深度定制)

这是将 Dify Workflow Builder 画布嵌入自有项目的方案,难度较高但定制空间无限。

11.1 前置准备:本地开发环境

bash 复制代码
# 1. Fork + Clone Dify 仓库
git clone https://github.com/langgenius/dify.git
cd dify/web

# 2. 安装依赖(推荐 pnpm)
pnpm install  # 或 npm install

# 3. 配置环境变量
cp .env.example .env.local
# 编辑 .env.local:
# NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api
# NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api

# 4. 启动开发服务器
pnpm dev  # → http://localhost:3000

11.2 修改前端代码生效的关键

yaml 复制代码
# docker-compose.yaml --- 必须改为本地构建,否则修改不生效!
services:
  web:
    # ❌ 删除或注释掉这行
    # image: langgenius/dify-web:1.13.3

    # ✅ 改为本地构建
    build:
      context: ../web
      dockerfile: Dockerfile
    # 或者直接挂载开发服务器:
    # command: pnpm dev
    # volumes:
    #   - ../web:/app
    #   - /app/node_modules

11.3 代码隔离约定(参考 dify-plus 实践)

复制代码
# 自定义代码使用 extend_ 前缀隔离,便于跟踪和升级维护
web/app/components/workflow/nodes/
├── extend_my-api-node/       ← 自定义节点目录(extend_ 前缀)
├── extend_database-node/
└── ...

# 修改记录追踪
git log --all --oneline web/app/components/workflow/

十二、自定义节点开发完整实战

以下实现一个"数据库查询节点",从指定数据库表查询数据并输出给下游节点。

12.1 后端:注册新节点类型

python 复制代码
# api/core/workflow/nodes/database_query/__init__.py
from .database_query_node import DatabaseQueryNode

NODE_TYPE_CLASSES_MAPPING = {
    # ... 现有节点
    'database-query': DatabaseQueryNode,
}
python 复制代码
# api/core/workflow/nodes/database_query/database_query_node.py
from core.workflow.nodes.base_node import BaseNode

class DatabaseQueryNode(BaseNode):
    _node_data_cls = DatabaseQueryNodeData

    def _run(self, variable_pool: VariablePool) -> NodeRunResult:
        node_data: DatabaseQueryNodeData = cast(self._node_data_cls, self.node_data)

        # 从变量池解析输入变量
        query_input = variable_pool.get(node_data.query_variable)

        # 执行数据库查询(实际业务逻辑)
        result = execute_db_query(
            connection_str=node_data.connection_string,
            query=node_data.sql_template.format(input=query_input)
        )

        return NodeRunResult(
            status=WorkflowNodeExecutionStatus.SUCCEEDED,
            inputs={"query_variable": str(query_input)},
            outputs={"result": result, "row_count": len(result)},
            process_data={}
        )

12.2 前端 types.ts

typescript 复制代码
// web/app/components/workflow/nodes/database-query/types.ts
import type { CommonNodeType, ValueSelector } from '../../types'

export type DatabaseQueryNodeType = CommonNodeType & {
  connection_string: string          // 数据库连接串(应使用环境变量)
  sql_template: string               // SQL 模板,支持 {input} 占位符
  query_variable: ValueSelector      // 引用上游节点的查询参数
  max_rows: number                   // 最大返回行数
}

12.3 前端 default.ts

typescript 复制代码
// web/app/components/workflow/nodes/database-query/default.ts
export const defaultConfig: Partial<DatabaseQueryNodeType> = {
  title: '数据库查询',
  desc: '',
  connection_string: '',
  sql_template: 'SELECT * FROM users WHERE name = \'{input}\' LIMIT 10',
  query_variable: [],
  max_rows: 100,
}

export const validate = (inputs: DatabaseQueryNodeType, t: TFunction): string[] => {
  const errors: string[] = []
  if (!inputs.connection_string) errors.push('连接字符串不能为空')
  if (!inputs.sql_template) errors.push('SQL 模板不能为空')
  if (!inputs.query_variable?.length) errors.push('查询变量不能为空')
  return errors
}

12.4 前端 use-config.ts

typescript 复制代码
// web/app/components/workflow/nodes/database-query/use-config.ts
import { useCallback } from 'react'
import { produce } from 'immer'
import useNodeCrud from '../_base/hooks/use-node-crud'
import type { DatabaseQueryNodeType } from './types'

const useConfig = (id: string, payload: DatabaseQueryNodeType) => {
  const { inputs, setInputs } = useNodeCrud<DatabaseQueryNodeType>(id, payload)

  const handleConnectionChange = useCallback((conn: string) => {
    setInputs(produce(inputs, d => { d.connection_string = conn }))
  }, [inputs, setInputs])

  const handleSqlChange = useCallback((sql: string) => {
    setInputs(produce(inputs, d => { d.sql_template = sql }))
  }, [inputs, setInputs])

  const handleQueryVarChange = useCallback((selector: ValueSelector) => {
    setInputs(produce(inputs, d => { d.query_variable = selector }))
  }, [inputs, setInputs])

  const handleMaxRowsChange = useCallback((rows: number) => {
    setInputs(produce(inputs, d => { d.max_rows = rows }))
  }, [inputs, setInputs])

  return { inputs, handleConnectionChange, handleSqlChange, handleQueryVarChange, handleMaxRowsChange }
}

export default useConfig

12.5 前端 panel.tsx

tsx 复制代码
// web/app/components/workflow/nodes/database-query/panel.tsx
import { memo } from 'react'
import Field from '../_base/components/field'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
import { VarType } from '../../types'
import useConfig from './use-config'

const Panel: FC<{ id: string; data: DatabaseQueryNodeType }> = memo(({ id, data }) => {
  const {
    inputs,
    handleConnectionChange, handleSqlChange,
    handleQueryVarChange, handleMaxRowsChange,
  } = useConfig(id, data)

  return (
    <div className="space-y-4 p-4">
      <Field title="数据库连接" required tooltip="使用环境变量引用敏感信息">
        <input
          value={inputs.connection_string}
          onChange={e => handleConnectionChange(e.target.value)}
          placeholder="postgresql://user:pass@host/db 或 {{env.DB_URL}}"
          className="input-field w-full"
        />
      </Field>

      <Field title="查询参数" required tooltip="从上游节点选择查询参数变量">
        <VarReferencePicker
          nodeId={id}
          value={inputs.query_variable}
          onChange={handleQueryVarChange}
          filterVar={v => [VarType.string, VarType.number].includes(v.type)}
        />
      </Field>

      <Field title="SQL 模板" required>
        <textarea
          value={inputs.sql_template}
          onChange={e => handleSqlChange(e.target.value)}
          rows={4}
          placeholder="SELECT * FROM table WHERE field = '{input}'"
          className="input-field w-full font-mono text-sm"
        />
      </Field>

      <Field title="最大返回行数">
        <input
          type="number"
          min={1}
          max={1000}
          value={inputs.max_rows}
          onChange={e => handleMaxRowsChange(Number(e.target.value))}
          className="input-field w-32"
        />
      </Field>
    </div>
  )
})

export default Panel

12.6 注册到节点类型表

typescript 复制代码
// web/app/components/workflow/nodes/index.ts
import DatabaseQueryNode from './database-query'
// ...
export const nodeTypes = {
  // ...existing,
  'database-query': DatabaseQueryNode,
}

// 在 block-selector 中加入节点面板选择项
export const allBlocksMap: Record<BlockEnum, BlockConfig> = {
  // ...
  'database-query': {
    type: 'database-query',
    title: '数据库查询',
    icon: <DatabaseIcon />,
    category: 'data',
    description: '从数据库查询数据并输出给下游节点',
  },
}

十三、将 Workflow 画布剥离嵌入自有项目

这是最复杂的方案,目标是将 ReactFlow 画布作为一个独立组件嵌入到你的 React 项目中

13.1 依赖安装

bash 复制代码
pnpm add @xyflow/react zustand immer
pnpm add -D @types/react @types/react-dom

13.2 最小化 Canvas 组件(从 Dify 提取)

tsx 复制代码
// src/components/workflow-canvas/index.tsx(你的项目)
import { ReactFlow, ReactFlowProvider, Background, Controls } from '@xyflow/react'
import '@xyflow/react/dist/style.css'
import { useWorkflowStore } from './store'
import { nodeTypes } from './node-types'    // 从 Dify 迁移的节点组件
import { CustomEdge } from './custom-edge'

interface WorkflowCanvasProps {
  initialNodes?: WorkflowNode[]
  initialEdges?: WorkflowEdge[]
  readonly?: boolean
  onSave?: (nodes: WorkflowNode[], edges: WorkflowEdge[]) => void
}

export const WorkflowCanvas: FC<WorkflowCanvasProps> = ({
  initialNodes = [],
  initialEdges = [],
  readonly = false,
  onSave,
}) => {
  const {
    nodes, edges,
    onNodesChange, onEdgesChange, onConnect,
  } = useWorkflowStore(initialNodes, initialEdges)

  return (
    <div style={{ width: '100%', height: '100%', minHeight: '600px' }}>
      <ReactFlowProvider>
        <ReactFlow
          nodes={nodes}
          edges={edges}
          nodeTypes={nodeTypes}
          edgeTypes={{ custom: CustomEdge }}
          onNodesChange={readonly ? undefined : onNodesChange}
          onEdgesChange={readonly ? undefined : onEdgesChange}
          onConnect={readonly ? undefined : onConnect}
          nodesDraggable={!readonly}
          nodesConnectable={!readonly}
          fitView
        >
          <Background />
          <Controls />
          {!readonly && (
            <button
              onClick={() => onSave?.(nodes, edges)}
              className="absolute top-4 right-4 z-10 px-4 py-2 bg-blue-600 text-white rounded-lg"
            >
              保存工作流
            </button>
          )}
        </ReactFlow>
      </ReactFlowProvider>
    </div>
  )
}

13.3 DSL 序列化(保存/加载工作流)

typescript 复制代码
// 将画布状态序列化为 Dify DSL 格式
export function serializeWorkflow(
  nodes: WorkflowNode[],
  edges: WorkflowEdge[]
): DifyWorkflowDSL {
  return {
    version: '0.6',
    kind: 'Workflow',
    graph: {
      nodes: nodes.map(node => ({
        id: node.id,
        type: node.data.type,
        position: node.position,
        data: omit(node.data, ['_runningStatus', '_connectedSourceHandleIds', ...]),
      })),
      edges: edges.map(edge => ({
        id: edge.id,
        source: edge.source,
        target: edge.target,
        sourceHandle: edge.sourceHandle,
        targetHandle: edge.targetHandle,
      })),
    },
  }
}

// 从 DSL 反序列化恢复画布
export function deserializeWorkflow(dsl: DifyWorkflowDSL): {
  nodes: WorkflowNode[]
  edges: WorkflowEdge[]
} {
  return {
    nodes: dsl.graph.nodes.map(n => ({
      id: n.id,
      type: n.type,
      position: n.position,
      data: { ...defaultConfig[n.type], ...n.data },
    })),
    edges: dsl.graph.edges.map(e => ({
      id: e.id,
      source: e.source,
      target: e.target,
      sourceHandle: e.sourceHandle,
      targetHandle: e.targetHandle,
      type: 'custom',
    })),
  }
}

13.4 在 Next.js / React 项目中使用

tsx 复制代码
// pages/workflow-editor.tsx
import { WorkflowCanvas } from '@/components/workflow-canvas'
import { serializeWorkflow } from '@/components/workflow-canvas/utils'

export default function WorkflowEditorPage() {
  const [savedDSL, setSavedDSL] = useState<string>('')

  const handleSave = (nodes: WorkflowNode[], edges: WorkflowEdge[]) => {
    const dsl = serializeWorkflow(nodes, edges)
    const yaml = dslToYaml(dsl)
    setSavedDSL(yaml)
    // 保存到后端 API
    fetch('/api/workflows', {
      method: 'POST',
      body: JSON.stringify({ dsl: yaml }),
    })
  }

  return (
    <div className="h-screen flex flex-col">
      <header className="h-14 border-b flex items-center px-6">
        <h1>工作流编辑器</h1>
      </header>
      <div className="flex-1">
        <WorkflowCanvas onSave={handleSave} />
      </div>
    </div>
  )
}

十四、常见避坑指南

14.1 修改代码不生效

bash 复制代码
# ❌ 问题:Docker 使用的是远程镜像
# ✅ 解决:切换为本地构建
docker compose down
# 修改 docker-compose.yaml,将 image 改为 build:
docker compose up -d --build

14.2 开始节点被删除

typescript 复制代码
// ❌ 直接使用 ReactFlow 默认删除
// ✅ 在 deleteNode action 中保护开始节点
deleteNode: (nodeId) => set((state) => {
  const node = state.nodes.find(n => n.id === nodeId)
  if (node?.data.type === BlockEnum.Start) return {}  // 受保护,直接返回
  return { nodes: state.nodes.filter(n => n.id !== nodeId) }
})

14.3 Windows 开发环境 localhost 访问问题

yaml 复制代码
# ❌ Docker 容器内访问 localhost 会报错
# ✅ 使用 host.docker.internal
NEXT_PUBLIC_API_PREFIX=http://host.docker.internal:5001/console/api

14.4 变量引用循环依赖

typescript 复制代码
// ❌ 节点 A 引用 B,节点 B 又引用 A → 造成 DAG 循环
// ✅ 在 onConnect 时检测是否引入环
const onConnect = (connection: Connection) => {
  if (wouldCreateCycle(connection, get().edges, get().nodes)) {
    toast.error('连接会导致循环依赖,已取消')
    return
  }
  set(state => ({ edges: addEdge(connection, state.edges) }))
}

14.5 大型工作流性能问题

tsx 复制代码
// ✅ 节点组件必须用 memo 包裹
const MyNode = memo<NodeProps<MyNodeType>>(({ data }) => {
  return <div>{data.title}</div>
})

// ✅ useSelector 精确订阅,避免全量重渲染
const nodeCount = useWorkflowStore(state => state.nodes.length)
// ❌ 不要这样(订阅整个 store)
const store = useWorkflowStore()

十五、总结

15.1 架构全景回顾

复制代码
ReactFlow 画布层
    ↕(onNodesChange / onEdgesChange / onConnect)
Zustand Store 层(nodes / edges / 运行态 / history)
    ↕(useNodeCrud / useWorkflow hooks)
节点 Panel 层(panel.tsx + use-config.ts)
    ↕(VarReferencePicker / Field / OutputVars)
变量引用系统(ValueSelector + VarType + 可用变量计算)
    ↕(SSE 流式推送)
工作流执行层(useWorkflowRun + EventSource)

15.2 二次开发路径选择

你的需求 推荐方案 预计工期
在网站嵌入 AI 对话 iframe / JS SDK < 1 天
自定义对话 UI API 直调 2-5 天
修改现有节点外观/配置 源码二开 + Docker 本地构建 1-3 天
新增自定义节点 四件套约定(前端+后端) 3-7 天
将画布嵌入自有 React 项目 提取 workflow 模块 + @xyflow/react 1-3 周
完全自研编排引擎 参考设计,基于 ReactFlow 从头实现 1-3 月

15.3 核心设计借鉴点

无论你选择哪种方案,以下设计值得在自己项目中复用:

  1. 四件套节点约定(panel / use-config / types / default)------ 新增节点成本极低
  2. ValueSelector 变量路径系统------ 类型安全的跨节点数据引用
  3. immer + useNodeCrud 模式 ------ 不可变状态更新简洁高效
  4. SSE 实时状态推送------ 节点运行态实时可视化
  5. 保护特殊节点的删除逻辑------ 防止用户误操作破坏流程结构

🔗 参考资源


基于 Dify v1.13.3 · @xyflow/react v12 · 欢迎交流前端架构设计经验!

相关推荐
NebulaData2 小时前
GPT-image 2 重磅上线,Nebula Lab 带您解锁 AI 创意新可能(附提示词版)
人工智能
宇宙realman_9992 小时前
DSP28335-FlashAPI使用
linux·前端·python
李可以量化2 小时前
Python之如何做出交易日历(上)
人工智能·算法·qmt·量化 qmt ptrade
与遨游于天地2 小时前
提示词技巧一览
人工智能
羊羊小栈2 小时前
基于「YOLO目标检测 + 多模态AI分析」的智慧植物辣椒病害智能检测分析预警系统
人工智能·yolo·目标检测·计算机视觉·毕业设计·大作业
litble2 小时前
如何速成LLM以伪装成一个AI研究者(2)——Pre-LN,KV-Cache优化,MoE
人工智能·大模型·llm·moe·kv-cache·pre-ln
zncxCOS2 小时前
【ETestDEV5教程40】代码开发之AI功能支持
人工智能·国产化·仿真测试·etest·嵌入式系统测试·测试开发平台
天堂12232 小时前
机器学习基本概念
人工智能·机器学习
阿里-于怀2 小时前
【无标题】阿里云 AI 网关支持 DeepSeek V4
人工智能·阿里云·云计算·deepseek