AI工作流节点前端扩展开发指南

本文档面向前端开发人员,详细介绍如何在 PigX UI 的 AI 流程编排系统中扩展新的节点类型。

快速开始:扩展新节点

步骤1:定义节点类型配置

src/views/knowledge/aiFlow/nodes/nodeTypes.ts 文件中添加新节点类型配置。

完整的节点配置示例

// 在 NodeType 接口中添加新节点的参数类型
interface NodeType extends Partial<Node> {
  type: string;
  name: string;
  category: string;        // 节点分类
  icon: string;            // Element Plus 图标名称
  description: string;     // 节点描述
  canBeSource: boolean;
  canBeTarget: boolean;

  // 添加你的新节点参数类型
  customParams?: {
    apiKey: string;
    endpoint: string;
  };
}

// 在 nodeTypes 数组中注册新节点
export const nodeTypes: NodeType[] = [
  // ...现有节点

  {
    type: 'custom',
    name: '自定义节点',
    category: nodeCategories.DATA_PROCESSING.key,
    icon: 'Setting',
    description: '自定义数据处理节点',
    canBeSource: true,
    canBeTarget: true,
    customParams: {
      apiKey: '',
      endpoint: ''
    },
    inputParams: [],
    outputParams: [
      { name: 'result', type: 'String' }
    ]
  }
];
配置生效时机

新增节点配置后,需要刷新浏览器才能在节点面板中看到新节点。动态组件加载机制会自动识别新的节点组件文件。

辅助函数说明nodeTypes.ts 第717-721行):

export const getNodeConfig = (type: string): NodeType => {
  const nodeConfig = nodeTypes.find(node => node.type === type) || nodeTypes[0];
  return deepClone(nodeConfig) as NodeType; // 深拷贝避免共享引用
};

步骤2:创建节点组件

创建 nodes/CustomNode.vue 文件,展示节点在画布上的视觉效果。

节点组件核心代码

<template>
  <div>
    <div class="output-params">
      <!-- 显示输入参数 -->
      <div v-for="(param, index) in inputParams" :key="index" class="param-item">
        <svg-icon :size="24" class="param-icon" name="local-var" />
        <span class="param-name">{{ param.name }}</span>
      </div>

      <!-- 显示节点特定信息 -->
      <div class="param-item">
        <svg-icon :size="24" class="param-icon" name="local-custom" />
        <span class="param-value">{{ node.customParams.endpoint }}</span>
      </div>
    </div>
  </div>
</template>

<script>
import common from './common.ts';

export default {
  name: 'CustomNode', // 组件名称必须与文件名一致
  mixins: [common] // 继承基础数据层
};
</script>

<style scoped>
// 样式省略,参考现有节点组件
</style>
Mixin 作用

nodes/common.ts mixin 提供了 inputParamsoutputParams 响应式属性,节点组件可以直接使用这些属性展示变量信息。

步骤3:创建配置面板

创建 panels/CustomPanel.vue 配置面板,用于配置节点参数。

配置面板核心代码

<template>
  <div class="panel-content">
    <!-- 输入变量:从上游节点选择变量 -->
    <div class="panel-section">
      <div class="panel-header">
        <span>变量输入</span>
        <el-button type="primary" size="small" @click="addParam">添加</el-button>
      </div>
      <div v-for="(param, index) in inputParams" :key="index">
        <el-input v-model="param.name" placeholder="变量名" />
        <el-select v-model="param.type" placeholder="变量值">
          <el-option-group v-for="item in previousOutputParams" :key="item.name" :label="item.name">
            <el-option v-for="p in item.list" :key="p.name" :value="`${item.id}.${p.name}`" />
          </el-option-group>
        </el-select>
        <!-- 删除按钮省略 -->
      </div>
    </div>

    <!-- 节点特定配置 -->
    <div class="panel-section">
      <el-input v-model="node.customParams.apiKey" placeholder="API Key" />
      <el-input v-model="node.customParams.endpoint" placeholder="API Endpoint" />
    </div>

    <!-- 输出变量:定义节点输出 -->
    <div class="panel-section">
      <div class="panel-header">
        <span>输出变量</span>
        <el-button type="primary" size="small" @click="addOutput">添加</el-button>
      </div>
      <!-- 输出变量列表省略,结构同输入变量 -->
    </div>
  </div>
</template>

<script>
import common from './common';

export default {
  name: 'CustomPanel',
  mixins: [common], // 继承变量系统层
  async mounted() {
    // 初始化节点特定参数
    if (!this.node.customParams) {
      this.$set(this.node, 'customParams', { apiKey: '', endpoint: '' });
    }
  },
  methods: {
    // 校验方法:关闭面板时自动调用
    validate() {
      const errors = [];
      if (!this.node.customParams?.apiKey) {
        errors.push({ field: 'apiKey', message: '请输入 API Key' });
      }
      return { valid: errors.length === 0, errors };
    }
  }
};
</script>

previousOutputParams 数据结构说明

// panels/common.ts 返回的结构
[
  {
    id: 'node_abc123',
    name: 'HTTP请求',
    list: [
      { name: 'body', type: 'Object' },
      { name: 'status_code', type: 'Number' }
    ]
  },
  {
    id: 'global',
    name: '全局环境变量',
    list: [
      { name: 'API_KEY', value: 'global' },
      { name: 'DB_URL', value: 'global' }
    ]
  },
  {
    id: 'loop',
    name: '循环变量',
    list: [
      { name: 'index', type: 'Number', description: '当前迭代索引' }
    ]
  }
]
校验方法

必须实现 validate() 方法,返回格式为 { valid: boolean, errors: Array } 的对象。关闭面板时会自动调用此方法。

步骤4:添加节点图标

系统使用 Element Plus 图标库,无需创建单独的 SVG 文件。

图标配置(在节点类型定义中):

{
  type: 'custom',
  name: '自定义节点',
  icon: 'Setting',  // Element Plus 图标名称
  // ...
}

图标使用(由 NodeItem.vue 自动渲染):

<svg-icon :size="24" :class="['node-icon', 'node-icon--' + node.type]" :name="`local-${node.type}`" />
图标库

可用图标列表参考 Element Plus 官方文档的 Icon 组件章节。

步骤5:配置节点样式

src/views/knowledge/aiFlow/styles/flow.scss 文件中为新节点添加样式配置。

完整的节点配色示例

.node-icon {
  margin-right: 8px;
  padding: 5px;
  border-radius: 5px;
  color: #fff;

  // 流程控制(蓝/黄色系)
  &--start { background-color: rgb(41, 109, 255); }
  &--end { background-color: #ef4444; }
  &--switch { background-color: rgb(255, 187, 0); }
  &--loop { background-color: rgb(255, 187, 0); }

  // AI能力(紫/蓝色系)
  &--llm { background-color: rgb(97, 114, 233); }
  &--question { background-color: rgb(27, 138, 106); }
  &--structured { background-color: rgb(107, 114, 218); }
  &--rag { background-color: rgb(75, 107, 251); }
  &--mcp { background-color: rgb(27, 138, 106); }
  &--ocr { background-color: rgb(156, 89, 182); }
  &--documentParser { background-color: rgb(129, 77, 77); }

  // 多模态(紫/橙色系)
  &--imageGen { background-color: rgb(147, 51, 234); }
  &--voiceGen { background-color: rgb(234, 88, 12); }

  // 数据处理(青/蓝色系)
  &--code { background-color: rgb(46, 114, 250); }
  &--text { background-color: rgb(27, 138, 106); }
  &--db { background-color: rgb(0, 206, 209); }

  // 外部集成(蓝/绿色系)
  &--http { background-color: rgb(135, 91, 247); }
  &--notice { background-color: rgb(64, 158, 255); }
  &--dify { background-color: rgb(22, 119, 255); }
  &--coze { background-color: rgb(0, 153, 255); }

  // 自定义节点
  &--custom { background-color: rgb(100, 150, 200); }
}

色系设计原则

分类色系语义
流程控制蓝/黄蓝色表示开始,黄色表示分支/循环
AI能力紫/蓝紫色表示深度AI处理,蓝色表示检索/分类
多模态紫/橙紫色表示图像处理,橙色表示音频处理
数据处理青/蓝青色表示数据库操作,蓝色表示代码执行
外部集成蓝/绿蓝色主导,绿色表示通知类操作

核心机制详解

变量系统完整机制

变量系统是节点间数据传递的核心机制,支持三个层级的变量来源。

变量作用域

变量的有效范围取决于节点连接关系。只有上游节点的输出变量才能被下游节点引用。循环变量仅在循环节点的子节点中可用。

变量来源的三个层级

层级1:上游节点输出

通过递归查找所有上游节点的输出参数。

// panels/common.ts 第86-104行
previousNodes(): Node[] {
  const nodes: Node[] = [];
  const findPreviousNodes = (nodeId: string): void => {
    const connection = this.actualParent.connections
      .find(conn => conn.targetId === nodeId);
    if (!connection) return;

    const node = this.actualParent.nodes
      .find(node => node.id === connection.sourceId);
    if (node) {
      nodes.push(node);
      findPreviousNodes(node.id); // 递归查找
    }
  };

  findPreviousNodes(this.node.id);
  return nodes;
}

层级2:全局环境变量

从工作流的环境变量配置中获取。

// panels/common.ts 第56-66行
if (this.actualParent.env && this.actualParent.env.length) {
  nodeOutputs.push({
    id: 'global',
    name: '全局环境变量',
    list: this.actualParent.env.map(env => ({
      name: env.name,
      value: 'global'
    }))
  });
}

层级3:循环变量

在循环节点的子节点中可用。

// panels/common.ts 第68-82行
if (this.childNodeParent?.loopNode) {
  const loopVarName = this.childNodeParent.loopVarName || 'index';
  nodeOutputs.push({
    id: 'loop',
    name: '循环变量',
    list: [
      {
        name: loopVarName,
        type: 'Number',
        description: '当前迭代索引(从0开始)'
      }
    ]
  });
}

变量引用格式

在节点配置中使用以下格式引用变量:

  • 上游节点输出:${nodeId}.${paramName}
  • 全局环境变量:${global}.${envName}
  • 循环变量:${loop}.${varName}

示例

// LLM 节点的消息配置
{
  messages: [
    {
      role: 'user',
      content: '你好,${nodeA.name},今天天气是${global.weather},这是第${loop.index}次迭代'
    }
  ]
}

数据传递流程

graph LR
  A[配置阶段] --> B[用户选择变量]
  B --> C[存储引用]
  C --> D[执行阶段]
  D --> E[解析变量]
  E --> F[模板替换]
  F --> G[获取实际值]

完整流程示例

  1. 配置阶段:用户在 Node B 的配置面板中选择 Node A 的输出参数 body
  2. 存储inputParams[0].type = "nodeA.body"
  3. 执行阶段
    • Node A 执行完成,结果存入 context.params["nodeA.body"] = { status: 200, data: {...} }
    • Node B 开始执行,从 context.params 中提取 nodeA.body 的值
    • 如果消息模板是 "${nodeA.body}",后端替换为实际值
actualParent 作用

actualParent 用于支持循环节点的子节点。在循环节点中配置子节点时,使用 childNodeParent 而非 parent

节点校验系统

节点校验系统确保用户在关闭配置面板前完成必填项配置。

面板级校验

每个配置面板组件都应实现 validate() 方法。

标准校验接口panels/common.ts 第167-169行):

validate(): ValidationResult {
  return { valid: true, errors: [] };
}

实际校验示例panels/MCPPanel.vue 第162-168行):

validate() {
  const errors = [];
  if (!this.node.mcpParams?.mcpId) {
    errors.push({ field: 'mcpId', message: '请选择MCP服务' });
  }
  return { valid: errors.length === 0, errors };
}

自动校验触发

校验在两个时机自动触发:

时机1:参数变化时panels/common.ts 第112-127行):

watch: {
  outputParams: {
    deep: true,
    handler() {
      this.updateVariables();
      this.updateValidationStatus();
    }
  },
  inputParams: {
    deep: true,
    handler() {
      this.updateVariables();
      this.updateValidationStatus();
    }
  }
},
mounted() {
  this.$nextTick(() => {
    this.updateValidationStatus();
  });
}

时机2:关闭面板时NodePanel.vue 第152-169行):

handleClose(done) {
  const panel = this.$refs.panelComponent;
  if (panel?.validate) {
    const result = panel.validate();
    this.node.hasValidationError = !result.valid;
    if (!result.valid) {
      this.$message.warning(result.errors[0]?.message || '请完善必填项');
    }
  }
  if (typeof done === 'function') done();
  this.$emit('close');
}
校验失败处理

如果校验失败,面板会显示警告消息,但不会阻止关闭。节点会标记为 hasValidationError = true,在画布上显示错误标识。

完整案例:MCP节点扩展

以下是 MCP(Model Context Protocol)节点扩展的关键实现差异。

节点类型配置

nodeTypes.ts 中添加 MCP 特定参数:

{
  type: 'mcp',
  name: 'MCP服务',
  category: nodeCategories.AI_CAPABILITY.key,
  icon: 'Puzzle',
  description: '调用模型上下文协议服务',
  canBeSource: true,
  canBeTarget: true,
  mcpParams: {  // MCP 特定参数
    mcpId: '',
    mcpName: '',
    prompt: ''
  },
  inputParams: [],
  outputParams: [
    { name: 'result', type: 'Object' },
    { name: 'status', type: 'String' }
  ]
}

节点组件实现

nodes/MCPNode.vue 遵循步骤2的模式,关键差异:

<template>
  <div class="param-item">
    <svg-icon :size="24" class="param-icon" name="local-mcp" />
    <!-- 显示 MCP 服务名称 -->
    <span class="param-value">{{ node.mcpParams.mcpName || node.mcpParams.mcpId }}</span>
  </div>
</template>

<script>
import common from './common.ts';
export default {
  name: 'McpNode',
  mixins: [common]
};
</script>

配置面板实现

panels/MCPPanel.vue 遵循步骤3的模式,关键差异在于 MCP 服务选择和 API 调用:

<script>
import common from './common';
import { list } from '/@/api/knowledge/aiMcpConfig';

export default {
  name: 'McpPanel',
  mixins: [common],
  data() {
    return {
      mcpList: [] // MCP 服务列表
    };
  },
  async mounted() {
    // 初始化 MCP 参数
    if (!this.node.mcpParams) {
      this.$set(this.node, 'mcpParams', { mcpId: '', mcpName: '', prompt: '' });
    }
    // 从后端加载 MCP 服务列表
    await this.fetchMcpList();
  },
  methods: {
    validate() {
      const errors = [];
      if (!this.node.mcpParams?.mcpId) {
        errors.push({ field: 'mcpId', message: '请选择MCP服务' });
      }
      return { valid: errors.length === 0, errors };
    },
    async fetchMcpList() {
      const { data } = await list();
      this.mcpList = data || [];
    },
    onMcpChange() {
      // 更新 MCP 服务名称
      const selectedMcp = this.mcpList.find(m => m.mcpId === this.node.mcpParams?.mcpId);
      if (selectedMcp) {
        this.node.mcpParams.mcpName = selectedMcp.name;
      }
    }
  }
};
</script>

样式配置

styles/flow.scss 中添加:

.node-icon {
  &--mcp {
    background-color: rgb(27, 138, 106);
  }
}

开发注意事项

命名规范

类型规范示例
节点类型标识小写字母mcp, httprequest, imagegen
节点组件名称大驼峰 + Node后缀McpNode, HttpRequestNode

系统架构深度解析

节点分类系统

AI Flow 采用了5大分类系统来组织17种内置节点类型,每个分类都有明确的功能定位和展开优先级。

节点分类配置nodes/nodeTypes.ts 第160-192行):

export const nodeCategories = {
  FLOW_CONTROL: {
    key: 'flow-control',
    label: '流程控制',
    order: 1,
    defaultCollapsed: false
  },
  AI_CAPABILITY: {
    key: 'ai-capability',
    label: 'AI能力',
    order: 2,
    defaultCollapsed: false
  },
  MULTIMODAL: {
    key: 'multimodal',
    label: '多模态',
    order: 3,
    defaultCollapsed: false
  },
  DATA_PROCESSING: {
    key: 'data-processing',
    label: '数据处理',
    order: 4,
    defaultCollapsed: false
  },
  EXTERNAL_INTEGRATION: {
    key: 'external-integration',
    label: '外部集成',
    order: 5,
    defaultCollapsed: false
  }
} as const;

17种内置节点类型分布

分类节点类型说明
流程控制start, end, switch, loop工作流的起始、结束、条件分支和循环控制
AI能力llm, question, structured, rag, mcp, ocr, documentParser大语言模型、问题分类、结构化输出、知识库检索等
多模态imageGen, voiceGen图像生成、语音生成
数据处理code, text, db代码执行、文本处理、数据库查询
外部集成http, notice, dify, cozeHTTP请求、消息通知、第三方平台集成

动态组件加载机制

系统使用 import.meta.glob 实现节点和面板组件的动态加载,无需手动注册。

节点组件动态加载NodeItem.vue 第80-86行):

const modules = import.meta.glob('./nodes/*.vue', { eager: true });

const nodeComponent = computed(() => {
  const componentName = `${props.node.type.charAt(0).toUpperCase()}${props.node.type.slice(1)}Node`;
  const component = Object.values(modules).find(module => module.default.name === componentName);
  return component?.default;
});

配置面板动态加载NodePanel.vue 第123-130行):

const panelModules = import.meta.glob('./panels/*.vue', { eager: true });

computed: {
  nodeConfig() {
    const panelName = `${this.node.type.charAt(0).toUpperCase()}${this.node.type.slice(1)}Panel`;
    const panel = Object.values(panelModules).find(m => m.default.name === panelName);
    return panel?.default;
  }
}
命名规范

节点组件必须命名为 {NodeType}Node.vue,配置面板必须命名为 {NodeType}Panel.vue,且组件的 name 属性必须与文件名一致(首字母大写)。

Mixin 继承体系

系统采用三层 Mixin 架构,实现从简单到复杂的功能复用。

三层 Mixin 职责划分

graph LR
  A[nodes/common.ts] --> B[基础数据层]
  D[panels/common.ts] --> E[变量系统层]
  H[mixins/Node.ts] --> I[执行逻辑层]
Mixin 文件行数主要功能使用场景
nodes/common.ts~20行提供 inputParams、outputParams 响应式属性节点组件(显示在画布上)
panels/common.ts~170行提供 previousNodes 递归、previousOutputParams 计算、参数管理方法配置面板组件(右侧抽屉)
mixins/Node.ts~615行提供 DFS 执行顺序计算、SSE 流式执行、连接状态管理画布设计器(index.vue)
深拷贝机制

getNodeConfig() 使用深拷贝返回节点配置,避免多个节点实例共享同一个配置对象。