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 提供了 inputParams 和 outputParams 响应式属性,节点组件可以直接使用这些属性展示变量信息。
步骤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[获取实际值]
完整流程示例:
- 配置阶段:用户在 Node B 的配置面板中选择 Node A 的输出参数
body
- 存储:
inputParams[0].type = "nodeA.body"
- 执行阶段:
- 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, coze | HTTP请求、消息通知、第三方平台集成 |
动态组件加载机制
系统使用 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() 使用深拷贝返回节点配置,避免多个节点实例共享同一个配置对象。