Skip to main content

创建新节点指南(前端)

概述

本指南将帮助您快速创建新的工作流节点。前端节点系统采用动态注册机制,只需按照标准结构创建文件即可自动注册。

核心概念

架构设计

每个节点包含三个核心文件:
my-custom-node/
├── config.ts          # 节点配置(定义 paramSchemas 和默认值)
├── FormComponent.vue  # 表单组件(使用 useVbenForm + 插槽)
└── index.vue          # 节点主组件(展示在画布上)

数据流

config.ts (paramSchemas)

FormComponent.vue (useVbenForm 渲染表单)

handleValuesChange (自动同步到 node.data.params)

后端接收 params 数据

创建步骤

1. 创建节点文件夹

langchat-ui/apps/langchat/src/views/workflows/node/ 目录下创建新文件夹:
mkdir my-custom-node

2. 创建节点配置 (config.ts)

基础结构

import type { WorkflowNode } from '../../types/nodeConfig';
import { GroupEnum, NodeTypeEnum } from '../../types/nodeConfig';

const config: WorkflowNode = {
  type: 'MyCustomNode',           // 节点类型(PascalCase + Node)
  nodeType: NodeTypeEnum.Common,  // 节点分类
  label: '我的自定义节点',         // 显示名称
  group: GroupEnum.Tool,           // 节点分组
  color: 'var(--lcui-color-violet)', // 颜色(推荐使用 CSS 变量)
  order: 0,                        // 排序权重
  description: '节点描述',         // 描述信息
  icon: 'svg:settings',            // 图标
  data: {
    paramSchemas: [],              // 表单字段配置(重要!)
    params: {},                    // 默认参数值(重要!)
    outputs: [],                   // 输出变量配置
  },
};

export default config;

paramSchemas 字段定义

paramSchemas 是表单的 JSON Schema 定义,每个字段包含:
paramSchemas: [
  {
    fieldName: 'fieldName',      // 字段名(必填)
    label: '标签名称',            // 表单标签(必填)
    help: '帮助提示',             // 帮助信息(可选)
    component: 'Input',          // 组件类型(必填)
    rules: 'required',           // 验证规则(可选)
    defaultValue: '',            // 默认值(可选)
    componentProps: {            // 组件属性(可选)
      placeholder: '请输入',
      // 其他组件特定属性...
    },
  },
]
支持的组件类型:
组件类型说明常用属性
Input输入框placeholder, type
Select下拉选择placeholder, options
Switch开关-
InputNumber数字输入placeholder, min, max
DatePicker日期选择-
RadioGroup单选组options
CheckboxGroup多选组options
InputPassword密码输入placeholder

表单字段联动(dependencies)

当需要实现表单字段的动态显示、禁用、必填等联动效果时,使用 dependencies 字段配置:
paramSchemas: [
  {
    fieldName: 'fieldName',
    label: '标签名称',
    component: 'Input',
    dependencies: {
      // 控制字段显示/隐藏 (函数返回 true 显示,false 隐藏)
      show(values) {
        return !!values.triggerField;  // 当 triggerField 为真时显示
      },

      // 控制字段禁用 (函数返回 true 禁用,false 启用)
      disabled(values) {
        return !!values.disabledSwitch;
      },

      // 控制字段必填 (函数返回 true 必填,false 非必填)
      required(values) {
        return !!values.requiredSwitch;
      },

      // 动态修改验证规则
      rules(values) {
        if (values.someField === 'specific') {
          return 'required';  // 特定条件下必填
        }
        return null;  // 恢复默认规则
      },

      // 动态修改组件属性(如下拉选项)
      componentProps(values) {
        if (values.otherField === 'option1') {
          return {
            options: [
              { label: '选项1', value: '1' },
              { label: '选项2', value: '2' },
              { label: '选项3', value: '3' },  // 添加新选项
            ],
          };
        }
        return {};  // 使用默认属性
      },

      // 指定哪些字段变化时触发依赖重新计算
      triggerFields: ['triggerField', 'disabledSwitch'],
    },
  },
]
dependencies 字段详解:
属性类型说明返回值
show函数控制字段显示/隐藏true 显示,false 隐藏
disabled函数控制字段禁用状态true 禁用,false 启用
required函数控制字段是否必填true 必填,false 非必填
rules函数动态修改验证规则验证规则字符串或对象
componentProps函数动态修改组件属性属性对象(会与默认属性合并)
triggerFields数组指定触发重新计算的字段字段名数组
关键要点:
  • triggerFields:只有指定的字段变化时才会触发 dependencies 重新计算,提高性能
  • 函数参数 values:当前表单所有字段的值对象
  • show vs 添加条件:使用 show 控制显示隐藏,效果为完全移除 DOM
  • disabled vs required:精细控制字段的禁用和必填状态
常见场景示例:
// 场景 1:选择后显示额外配置字段
{
  fieldName: 'advancedConfig',
  label: '高级配置(根据需求显示)',
  component: 'Input',
  dependencies: {
    show(values) {
      return values.enableAdvanced === true;  // enableAdvanced 开启时显示
    },
    triggerFields: ['enableAdvanced'],
  },
}

// 场景 2:根据选择的类型动态改变下拉选项
{
  fieldName: 'subType',
  label: '子类型',
  component: 'Select',
  dependencies: {
    componentProps(values) {
      if (values.mainType === 'typeA') {
        return {
          options: [
            { label: 'A-1', value: 'a1' },
            { label: 'A-2', value: 'a2' },
          ],
        };
      } else if (values.mainType === 'typeB') {
        return {
          options: [
            { label: 'B-1', value: 'b1' },
            { label: 'B-2', value: 'b2' },
          ],
        };
      }
      return {};
    },
    triggerFields: ['mainType'],
  },
}

// 场景 3:条件必填
{
  fieldName: 'details',
  label: '详细信息',
  component: 'Input',
  dependencies: {
    required(values) {
      // 当选择了"详细"选项时才必填
      return values.level === 'detailed';
    },
    triggerFields: ['level'],
  },
}

// 场景 4:根据其他字段值动态验证
{
  fieldName: 'confirmPassword',
  label: '确认密码',
  component: 'InputPassword',
  dependencies: {
    rules(values) {
      // 自定义验证:必须与密码字段一致
      if (values.confirmPassword !== values.password) {
        return z.string().refine(
          () => false,
          { message: '两次输入的密码不一致' }
        );
      }
      return z.string().optional();
    },
    triggerFields: ['password', 'confirmPassword'],
  },
}

params 默认值

params 对象定义了表单的默认值,字段名必须与 paramSchemas 中的 fieldName 一致
params: {
  fieldName: '',           // 与 paramSchemas 对应
  modelId: undefined,      // 模型选择器默认为 undefined
  input: '{{#sys.message#}}', // 可以使用变量引用
  switch: true,            // 布尔值
  number: 10,              // 数字
}

完整示例

const config: WorkflowNode = {
  type: 'LlmNode',
  nodeType: NodeTypeEnum.Common,
  label: 'LLM',
  group: GroupEnum.AI,
  color: 'var(--lcui-color-violet)',
  description: '使用AI大模型对话或生成文本',
  icon: 'svg:brain',
  data: {
    paramSchemas: [
      {
        fieldName: 'modelId',
        label: 'AI模型',
        help: '选择要使用的AI模型',
        componentProps: {
          placeholder: '请选择AI模型',
        },
        rules: 'required',
        component: 'Select',
      },
      {
        fieldName: 'input',
        label: '输入内容',
        help: 'AI要回答的文本内容',
        componentProps: {
          placeholder: '请输入内容',
        },
        rules: 'required',
        component: 'Input',
      },
      {
        fieldName: 'promptText',
        label: '提示词',
        help: '系统提示词',
        componentProps: {
          type: 'textarea',
          placeholder: '请输入提示词',
        },
        component: 'Input',
      },
      {
        fieldName: 'stream',
        label: '是否流式输出',
        help: '流式输出将直接输出给用户',
        componentProps: {
          placeholder: '请选择是否流式输出',
        },
        rules: 'required',
        component: 'Switch',
      },
    ],
    params: {
      modelId: undefined,
      input: '{{#sys.message#}}',
      promptText: '',
      stream: true,
    },
    outputs: [
      { field: 'text', type: 'string', description: '生成内容' },
    ],
  },
};

3. 创建表单组件 (FormComponent.vue)

基础模板

<script lang="ts" setup>
import { onMounted } from 'vue';

import { useVbenForm } from '#/adapter/form';
import NodeValidationMixin from '#/views/workflows/common/NodeValidationMixin.vue';

const props = defineProps({
  node: {
    type: Object,
    required: true,
  },
});

const [BaseForm, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
  },
  showDefaultActions: false,
  layout: 'vertical',
  schema: config.data.paramSchemas,  // 使用 config.ts 中定义的 schema
  wrapperClass: 'grid-cols-1',
  handleValuesChange: async (values) => {
    props.node.data.params = values;  // 自动同步到 params
  },
});

onMounted(async () => {
  await formApi.setValues(props.node.data.params);  // 初始化表单值
});
</script>

<template>
  <NodeValidationMixin
    :form-api="formApi"
    :node-id="node.id"
    :node-type="node.type"
    :validation-message="`请完善${node.label}节点的必填配置项`"
  >
    <div class="flex flex-col gap-2">
      <div class="px-1">
        <BaseForm>
          <!-- 通过插槽自定义特定字段的渲染 -->
        </BaseForm>
      </div>
    </div>
  </NodeValidationMixin>
</template>

插槽自定义渲染

当需要自定义某个字段的渲染时,使用插槽:
<BaseForm>
  <!-- 插槽名称 = paramSchemas 中的 fieldName -->
  <template #fieldName="field">
    <CustomComponent v-bind="field" />
  </template>
</BaseForm>
插槽名称必须与 paramSchemas 中的 fieldName 一致!

4. 常用插槽类型

4.1 模型选择器 (modelId)

用于选择 AI 模型:
<script setup>
import { ModelSelector } from '#/components';
</script>

<template>
  <BaseForm>
    <template #modelId="field">
      <ModelSelector :show-config="false" v-bind="field" />
    </template>
  </BaseForm>
</template>
ModelSelector 组件属性:
属性类型说明默认值
modelValue / v-modelstring选中的模型ID-
typeModelTypeEnum模型类型过滤text2text
providerstring供应商过滤-
showConfigboolean是否显示参数配置面板true
config / v-model:configModelConfig模型参数配置-
config.ts 中的定义:
{
  fieldName: 'modelId',
  label: 'AI模型',
  rules: 'required',
  component: 'Select',
}

4.2 变量引用输入 (input)

用于引用其他节点的输出变量:
<script setup>
import VarReferenceInput from '#/views/workflows/common/var-reference/VarReferenceInput.vue';
</script>

<template>
  <BaseForm>
    <template #input="field">
      <VarReferenceInput :node="node" v-bind="field" />
    </template>
  </BaseForm>
</template>
config.ts 中的定义:
{
  fieldName: 'input',
  label: '输入内容',
  rules: 'required',
  component: 'Input',
}

4.3 提示词选择器 (promptText)

支持选择预设提示词或手动输入:
<script setup>
import PromptSelector from '#/components/PromptSelector/index.vue';
import VarReferenceInput from '#/views/workflows/common/var-reference/VarReferenceInput.vue';
</script>

<template>
  <BaseForm>
    <template #promptText="field">
      <div class="flex w-full flex-col gap-2">
        <PromptSelector mode="popover" v-bind="field" />
        <VarReferenceInput :node="node" v-bind="field" />
      </div>
    </template>
  </BaseForm>
</template>

4.4 自定义复杂组件

当需要处理复杂的数据结构(如动态列表、嵌套对象)时,创建独立组件:
<!-- FormComponent.vue -->
<script setup>
import DynamicVariables from './DynamicVariables.vue';
</script>

<template>
  <BaseForm>
    <template #variables="field">
      <DynamicVariables :node="node" v-bind="field" />
    </template>
  </BaseForm>
</template>
自定义组件要求:
  • 必须支持 v-model 双向绑定(通过 modelValueupdate:modelValue
  • 接收 node prop 以访问节点数据
  • 使用 v-bind="field" 传递表单字段属性
DynamicVariables.vue 示例:
<script setup>
const props = defineProps<{
  node: any;
  modelValue?: any[];
}>();

const emit = defineEmits<{
  'update:modelValue': [value: any[]];
}>();

// 本地状态
const items = ref([...props.modelValue]);

// 监听变化并向上emit
watch(items, (newVal) => {
  emit('update:modelValue', newVal);
  // 同步到node.data.params
  props.node.data.params.fieldName = newVal;
}, { deep: true });
</script>

5. 创建节点主组件 (index.vue)

节点主组件在画布上展示,结构固定:
<script lang="ts" setup>
import { useNode } from '@vue-flow/core';

import NodeArgInfoCard from '#/views/workflows/common/NodeArgInfoCard.vue';
import NodeLayout from '#/views/workflows/layout/NodeLayout.vue';

import FormComponent from './FormComponent.vue';

const { node } = useNode();
</script>

<template>
  <NodeLayout :node="node">
    <template #content>
      <div class="flex flex-col gap-2">
        <!-- 表单配置 -->
        <FormComponent :node="node" />

        <!-- 输出变量展示 -->
        <NodeArgInfoCard :args="node.data.outputs" title="输出变量" />
      </div>
    </template>
  </NodeLayout>
</template>

完整示例

基础节点参考: 查看 llm-node 了解简单节点的完整实现 字段联动示例: 参考 form-generator-node/FormComponent.vue(演示 dependencies 用法) 字段联动(dependencies)的常见场景:
  • 条件显示:根据其他字段值决定是否显示某个字段
  • 条件必填:根据其他字段值动态修改必填状态
  • 条件禁用:根据其他字段值控制字段是否禁用
  • 动态选项:根据其他字段值改变下拉选择的可选项

配置参考

节点分类 (nodeType)

说明
NodeTypeEnum.Common普通节点
NodeTypeEnum.Start开始节点
NodeTypeEnum.End结束节点
NodeTypeEnum.IF条件节点

节点分组 (group)

说明
GroupEnum.AIAI能力
GroupEnum.Base基础节点
GroupEnum.Tool工具
GroupEnum.Model多模态
GroupEnum.Output输入&输出
GroupEnum.Message消息渠道

验证规则 (rules)

规则说明
'required'必填
z.string().email()邮箱格式
z.string().min(6)最小长度
z.number().min(0).max(100)数字范围

开发规范

1. 命名规范

  • 文件夹:kebab-case(如 my-custom-node
  • 节点类型:PascalCase + Node(如 MyCustomNode
  • 文件名:PascalCase(如 FormComponent.vue

2. 必须规则

  • paramSchemas 定义表单字段
  • params 定义默认值(字段名必须与 paramSchemas 一致)
  • FormComponent.vue 使用 useVbenForm
  • ✅ 通过 handleValuesChange 自动同步数据
  • ✅ 使用插槽自定义复杂组件
  • ✅ 包裹在 NodeValidationMixin 中进行验证

3. 禁止事项

  • ❌ 不要在 FormComponent 中直接使用 NaiveUI 表单组件(应通过 paramSchemas 定义)
  • ❌ 不要手动同步表单值(应使用 handleValuesChange)
  • ❌ 不要在 params 中定义 paramSchemas 没有的字段
  • ❌ 不要忘记在 onMounted 中初始化表单值

4. 插槽命名

插槽名称 必须paramSchemas 中的 fieldName 完全一致:
// config.ts
{
  fieldName: 'myField',  // ← 这里
  // ...
}
<!-- FormComponent.vue -->
<template #myField="field">  <!-- ← 必须一致 -->
  <CustomComponent v-bind="field" />
</template>

测试与验证

1. 检查节点注册

  • 刷新页面,节点应该自动出现在节点面板中
  • 检查节点的分组、颜色、图标是否正确

2. 测试表单功能

  • 拖拽节点到画布
  • 测试表单字段的输入、验证
  • 检查 node.data.params 是否正确更新
  • 测试保存和加载功能

3. 调试技巧

// 在 handleValuesChange 中打印
handleValuesChange: async (values) => {
  console.log('[FormComponent] 表单值变化:', values);
  props.node.data.params = values;
}

// 在 onMounted 中检查初始值
onMounted(async () => {
  console.log('[FormComponent] 初始参数:', props.node.data.params);
  await formApi.setValues(props.node.data.params);
});

常见问题

Q: 表单值没有同步到 node.data.params?

A: 检查 handleValuesChange 是否正确配置,确保 props.node.data.params = values

Q: 插槽不生效?

A: 检查插槽名称是否与 paramSchemas 中的 fieldName 完全一致

Q: 表单验证失败?

A: 检查 paramSchemas 中的 rules 配置,确保必填字段有默认值

Q: 自定义组件数据不同步?

A: 确保自定义组件支持 v-model(实现 modelValue prop 和 update:modelValue emit)

节点输出配置

输出类型概述

节点输出分为两种类型:
类型说明使用场景
静态输出输出字段固定不变LLM 节点始终输出 text;始终输出相同的多个字段
动态输出输出字段由用户配置决定参数提取器节点、表单生成器节点等

静态输出配置

config.ts 中使用 data.outputs 定义固定的输出字段:
const config: WorkflowNode = {
  type: 'LlmNode',
  data: {
    paramSchemas: [...],
    params: {...},
    // 静态输出示例
    outputs: [
      { field: 'text', type: 'string', description: '生成内容' },
      { field: 'status', type: 'number', description: '状态码' },
    ],
  },
};

动态输出配置

当输出字段由用户在表单中配置时,在业务代码中直接操作 node.data.outputs 数组: 参考实现:
  • form-generator-node/FormComponent.vue - 在 updateDynamicOutputs() 函数中直接操作 outputs 数组
  • parameter-extractor-node/ExtractionVariables.vue - 在 syncOutputs() 函数中直接操作 outputs 数组
基本模式:
// 在自定义组件中,监听用户配置的变化
watch(userDefinedFields, () => {
  // 直接修改 outputs 数组
  props.node.data.outputs = userDefinedFields.map((field) => ({
    field: field.name,
    type: field.type,
    description: field.description,
  }));
}, { deep: true });

变量引用格式

前端通过以下格式引用节点输出变量:
{{#alias.field#}}
示例:
  • {{#llm_a1b2.text#}} - 引用 LLM 节点的 text 输出
  • {{#extract_7c9d.age#}} - 引用参数提取器节点的 age 字段
  • {{#sys.message#}} - 引用系统消息

参考资源

  • 示例节点llm-nodecode-nodevariable-update-node
  • 动态输出示例parameter-extractor-node
  • 公共组件#/views/workflows/common/
  • 输出解析器#/views/workflows/utils/outputResolver.ts
  • 表单适配器#/adapter/form
  • Vue Flow 文档https://vueflow.dev

总结

前端节点创建的核心流程:
  1. config.ts:定义 paramSchemas(表单结构)、params(默认值)和 data.outputs(输出字段)
  2. FormComponent.vue:使用 useVbenForm 渲染表单,通过 handleValuesChange 自动同步数据到 node.data.params
  3. 插槽自定义:通过 #fieldName 插槽自定义复杂组件(模型选择、变量引用、动态列表等)
  4. 动态输出:当用户配置的字段影响输出时,在自定义组件中直接修改 node.data.outputs 数组
  5. index.vue:使用 NodeLayoutNodeArgInfoCard 展示节点及其输出变量

快速参考

场景参考文件
基础节点llm-node/config.tsllm-node/FormComponent.vue
字段联动form-generator-node/FormComponent.vue(dependencies 用法)
动态输出form-generator-node/FormComponent.vueparameter-extractor-node/ExtractionVariables.vue
模型选择参考 llm-node 中的 ModelSelector 插槽
变量引用参考 llm-node 中的 VarReferenceInput 插槽
遵循这个流程和参考现有的节点实现,就能快速创建符合项目规范的工作流节点!